Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add range validation to number field #6575

Merged
merged 17 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { Controller, RegisterOptions } from 'react-hook-form'
import { FormControl, SimpleGrid } from '@chakra-ui/react'
import { extend, isEmpty, pick } from 'lodash'

import { NumberFieldBase, NumberSelectedValidation } from '~shared/types/field'
import {
NumberFieldBase,
NumberSelectedLengthValidation,
NumberSelectedValidation,
} from '~shared/types/field'

import { createBaseValidationRules } from '~utils/fieldValidation'
import { SingleSelect } from '~components/Dropdown'
Expand All @@ -25,44 +29,112 @@ type EditNumberProps = EditFieldProps<NumberFieldBase>

const EDIT_NUMBER_FIELD_KEYS = ['title', 'description', 'required'] as const

// As we want to keep the values in the shared type simple,
// we create a separate enum for frontend options and transform them as needed
enum NumberSelectedValidationInputs {
Length = 'Character Length',
Range = 'Number Range',
}

type EditNumberInputs = Pick<
NumberFieldBase,
typeof EDIT_NUMBER_FIELD_KEYS[number]
> & {
ValidationOptions: {
selectedValidation: NumberSelectedValidation | ''
customVal: number | ''
selectedValidation: NumberSelectedValidationInputs | ''
LengthValidationOptions: {
customVal: number | ''
selectedLengthValidation: NumberSelectedLengthValidation | ''
}
RangeValidationOptions: {
customMin: number | ''
customMax: number | ''
}
}
}

const transformNumberFieldToEditForm = (
field: NumberFieldBase,
): EditNumberInputs => {
const nextValidationOptions = {
selectedValidation:
field.ValidationOptions.selectedValidation || ('' as const),
const {
selectedValidation,
LengthValidationOptions,
RangeValidationOptions,
} = field.ValidationOptions

const nextSelectedValidation =
selectedValidation === NumberSelectedValidation.Length
? NumberSelectedValidationInputs.Length
: selectedValidation === NumberSelectedValidation.Range
? NumberSelectedValidationInputs.Range
: ('' as const)

const nextLengthValidationOptions = {
selectedLengthValidation:
LengthValidationOptions.selectedLengthValidation || ('' as const),
customVal:
(!!field.ValidationOptions.selectedValidation &&
field.ValidationOptions.customVal) ||
(!!LengthValidationOptions.selectedLengthValidation &&
LengthValidationOptions.customVal) ||
('' as const),
}

const nextRangeValidationOptions = {
customMin: RangeValidationOptions.customMin || ('' as const),
customMax: RangeValidationOptions.customMax || ('' as const),
}

return {
...pick(field, EDIT_NUMBER_FIELD_KEYS),
ValidationOptions: nextValidationOptions,
ValidationOptions: {
selectedValidation: nextSelectedValidation,
LengthValidationOptions: nextLengthValidationOptions,
RangeValidationOptions: nextRangeValidationOptions,
},
}
}

const transformNumberEditFormToField = (
inputs: EditNumberInputs,
originalField: NumberFieldBase,
): NumberFieldBase => {
const nextValidationOptions =
inputs.ValidationOptions.selectedValidation === ''
? {
selectedValidation: null,
customVal: null,
const {
selectedValidation,
LengthValidationOptions,
RangeValidationOptions,
} = inputs.ValidationOptions

const hasSelectedLengthValidationOption =
selectedValidation === NumberSelectedValidationInputs.Length &&
LengthValidationOptions.selectedLengthValidation !== ''

const nextSelectedValidation =
selectedValidation === NumberSelectedValidationInputs.Length
? NumberSelectedValidation.Length
: selectedValidation === NumberSelectedValidationInputs.Range
? NumberSelectedValidation.Range
: null

const nextLengthValidationOptions = hasSelectedLengthValidationOption
? LengthValidationOptions
: {
selectedLengthValidation: null,
customVal: null,
}

const nextRangeValidationOptions =
selectedValidation === NumberSelectedValidationInputs.Range
? RangeValidationOptions
: {
customMin: null,
customMax: null,
}
: inputs.ValidationOptions

const nextValidationOptions = {
selectedValidation: nextSelectedValidation,
LengthValidationOptions: nextLengthValidationOptions,
RangeValidationOptions: nextRangeValidationOptions,
}

return extend({}, originalField, inputs, {
ValidationOptions: nextValidationOptions,
})
Expand Down Expand Up @@ -98,24 +170,26 @@ export const EditNumber = ({ field }: EditNumberProps): JSX.Element => {
'ValidationOptions.selectedValidation',
)

const customValValidationOptions: RegisterOptions<
const watchedSelectedLengthValidation = watch(
'ValidationOptions.LengthValidationOptions.selectedLengthValidation',
)

const LengthCustomValValidationOptions: RegisterOptions<
LeonardYam marked this conversation as resolved.
Show resolved Hide resolved
EditNumberInputs,
'ValidationOptions.customVal'
'ValidationOptions.LengthValidationOptions.customVal'
> = useMemo(
() => ({
// customVal is required if there is selected validation.
validate: {
hasValidation: (val) => {
return (
!!val ||
!getValues('ValidationOptions.selectedValidation') ||
!getValues(
'ValidationOptions.LengthValidationOptions.selectedLengthValidation',
) ||
'Please enter number of characters'
)
},
validNumber: (val) => {
// Check whether input is a valid number, avoid e
return !isNaN(Number(val)) || 'Please enter a valid number'
},
},
min: {
value: 1,
Expand All @@ -129,11 +203,38 @@ export const EditNumber = ({ field }: EditNumberProps): JSX.Element => {
[getValues],
)

// We use the customMin field to perform cross-field validation for
// the number range
const RangeMinimumValidationOptions: RegisterOptions<
EditNumberInputs,
'ValidationOptions.RangeValidationOptions.customMin'
> = useMemo(
() => ({
validate: {
hasValidRange: (val) => {
const numVal = Number(val)
const customMax = getValues(
'ValidationOptions.RangeValidationOptions.customMax',
)

return (
customMax === '' ||
numVal <= Number(customMax) ||
`Please enter a valid range!`
)
},
},
}),
[getValues],
)

// Effect to clear validation option errors when selection limit is toggled off.
justynoh marked this conversation as resolved.
Show resolved Hide resolved
useEffect(() => {
if (!watchedSelectedValidation) {
clearErrors('ValidationOptions')
setValue('ValidationOptions.customVal', '')
setValue('ValidationOptions.LengthValidationOptions.customVal', '')
setValue('ValidationOptions.RangeValidationOptions.customMin', '')
setValue('ValidationOptions.RangeValidationOptions.customMax', '')
}
}, [clearErrors, setValue, watchedSelectedValidation])

Expand All @@ -160,42 +261,115 @@ export const EditNumber = ({ field }: EditNumberProps): JSX.Element => {
isReadOnly={isLoading}
isInvalid={!isEmpty(errors.ValidationOptions)}
>
<FormLabel isRequired>Number of characters allowed</FormLabel>
<SimpleGrid
mt="0.5rem"
columns={{ base: 2, md: 1, lg: 2 }}
spacing="0.5rem"
>
<Controller
name="ValidationOptions.selectedValidation"
control={control}
render={({ field }) => (
<SingleSelect
items={Object.values(NumberSelectedValidation)}
{...field}
<FormLabel isRequired>
Validate by character length or number range
</FormLabel>
<Controller
name="ValidationOptions.selectedValidation"
control={control}
render={({ field }) => (
<SingleSelect
items={Object.values(NumberSelectedValidationInputs)}
{...field}
/>
)}
/>
{watchedSelectedValidation === NumberSelectedValidationInputs.Range && (
<>
<FormLabel isRequired mt="0.5rem">
Minimum and/or maximum value
</FormLabel>
<SimpleGrid
mt="0.5rem"
columns={{ base: 2, md: 1, lg: 2 }}
spacing="0.5rem"
>
<Controller
name="ValidationOptions.RangeValidationOptions.customMin"
control={control}
rules={RangeMinimumValidationOptions}
render={({ field: { onChange, ...rest } }) => (
<NumberInput
inputMode="numeric"
showSteppers={false}
placeholder="Minimum"
onChange={validateNumberInput(onChange)}
{...rest}
/>
)}
/>
<Controller
name="ValidationOptions.RangeValidationOptions.customMax"
control={control}
render={({ field: { onChange, ...rest } }) => (
<NumberInput
inputMode="numeric"
showSteppers={false}
placeholder="Maximum"
onChange={validateNumberInput(onChange)}
{...rest}
/>
)}
/>
</SimpleGrid>
<FormErrorMessage>
{
errors?.ValidationOptions?.RangeValidationOptions?.customMin
?.message
}
</FormErrorMessage>
</>
)}
{watchedSelectedValidation ===
NumberSelectedValidationInputs.Length && (
<>
<FormLabel isRequired mt="0.5rem">
Number of characters allowed
</FormLabel>
<SimpleGrid
mt="0.5rem"
columns={{ base: 2, md: 1, lg: 2 }}
spacing="0.5rem"
>
<Controller
name="ValidationOptions.LengthValidationOptions.selectedLengthValidation"
control={control}
render={({ field }) => (
<SingleSelect
items={Object.values(NumberSelectedLengthValidation)}
{...field}
/>
)}
/>
)}
/>
<Controller
name="ValidationOptions.customVal"
control={control}
rules={customValValidationOptions}
render={({ field: { onChange, ...rest } }) => (
<NumberInput
flex={1}
inputMode="numeric"
showSteppers={false}
placeholder="Number of characters"
isDisabled={!watchedSelectedValidation}
onChange={validateNumberInput(onChange)}
{...rest}
<Controller
name="ValidationOptions.LengthValidationOptions.customVal"
control={control}
rules={LengthCustomValValidationOptions}
render={({ field: { onChange, ...rest } }) => (
<NumberInput
flex={1}
inputMode="numeric"
showSteppers={false}
placeholder="Number of characters"
isDisabled={!watchedSelectedLengthValidation}
onChange={validateNumberInput(onChange)}
{...rest}
/>
)}
/>
)}
/>
</SimpleGrid>
<FormErrorMessage>
{errors?.ValidationOptions?.customVal?.message}
</FormErrorMessage>
</SimpleGrid>
<FormErrorMessage>
{
errors?.ValidationOptions?.LengthValidationOptions
?.selectedLengthValidation?.message
}
{
errors?.ValidationOptions?.LengthValidationOptions?.customVal
?.message
}
</FormErrorMessage>
</>
)}
</FormControl>
<FormFieldDrawerActions
isLoading={isLoading}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ export const getFieldCreationMeta = (fieldType: BasicField): FieldCreateDto => {
}
}
case BasicField.LongText:
case BasicField.Number: {
return {
fieldType,
...baseMeta,
Expand All @@ -102,6 +101,22 @@ export const getFieldCreationMeta = (fieldType: BasicField): FieldCreateDto => {
customVal: null,
},
}
case BasicField.Number: {
return {
fieldType,
...baseMeta,
ValidationOptions: {
selectedValidation: null,
LengthValidationOptions: {
selectedLengthValidation: null,
customVal: null,
},
RangeValidationOptions: {
customMin: null,
customMax: null,
},
},
}
}
case BasicField.Dropdown: {
return {
Expand Down
Loading