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(v2): edit checkbox field in form builder #3581

Merged
merged 16 commits into from
Mar 21, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { FIELD_LIST_DROP_ID } from '../constants'
import { DndPlaceholderProps } from '../types'
import {
setToInactiveSelector,
stateDataSelector,
useBuilderAndDesignStore,
} from '../useBuilderAndDesignStore'

Expand All @@ -25,7 +24,6 @@ export const BuilderAndDesignContent = ({
placeholderProps,
}: BuilderAndDesignContentProps): JSX.Element => {
const setFieldsToInactive = useBuilderAndDesignStore(setToInactiveSelector)
const stateData = useBuilderAndDesignStore(stateDataSelector)
const { builderFields } = useBuilderFields()

useEffect(() => setFieldsToInactive, [setFieldsToInactive])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const BuilderAndDesignDrawer = (): JSX.Element | null => {
overflow="hidden"
{...DRAWER_MOTION_PROPS}
>
<Flex w="100%" h="100%" minW="max-content" flexDir="column">
<Flex w="100%" h="100%" flexDir="column">
{renderDrawerContent}
</Flex>
</MotionBox>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import IconButton from '~components/IconButton'

import { BASICFIELD_TO_DRAWER_META } from '~features/admin-form/create/constants'

import { useBuilderFields } from '../../BuilderAndDesignContent/useBuilderFields'
import { useCreateFormField } from '../../mutations/useCreateFormField'
import { useEditFormField } from '../../mutations/useEditFormField'
import {
Expand All @@ -21,7 +22,7 @@ import {
import { CreatePageDrawerCloseButton } from '../CreatePageDrawerCloseButton'

import { FieldMutateOptions } from './edit-fieldtype/common/types'
import { EditHeader } from './edit-fieldtype/EditHeader'
import { EditCheckbox, EditHeader } from './edit-fieldtype'

export const EditFieldDrawer = (): JSX.Element | null => {
const { stateData, setToInactive, updateEditState, updateCreateState } =
Expand Down Expand Up @@ -79,6 +80,26 @@ export const EditFieldDrawer = (): JSX.Element | null => {
[stateData, updateCreateState, updateEditState],
)

// Hacky method of determining when to rerender the drawer,
// i.e. when the user clicks into a different field.
// We pass `${fieldIndex}-${numFields}` as the key. If the
// user was creating a new field but clicked into an existing
// field, causing the new field to be discarded, then numFields
// changes. If the user was editing an existing field then clicked
// into another existing field, causing the edits to be discarded,
// then fieldIndex changes.
const { builderFields } = useBuilderFields()
const fieldIndex = useMemo(() => {
if (stateData.state === BuildFieldState.CreatingField) {
return stateData.insertionIndex
} else if (stateData.state === BuildFieldState.EditingField) {
return builderFields?.findIndex(
(field) => field._id === stateData.field._id,
)
}
}, [builderFields, stateData])
const numFields = useMemo(() => builderFields?.length, [builderFields])

if (!fieldToEdit) return null

return (
Expand Down Expand Up @@ -111,6 +132,7 @@ export const EditFieldDrawer = (): JSX.Element | null => {
handleChange={handleChange}
handleSave={handleSave}
handleCancel={setToInactive}
key={`${fieldIndex}-${numFields}`}
/>
</>
)
Expand All @@ -131,6 +153,8 @@ const MemoFieldDrawerContent = memo((props: MemoFieldDrawerContentProps) => {
switch (field.fieldType) {
case BasicField.Section:
return <EditHeader {...props} field={field} />
case BasicField.Checkbox:
return <EditCheckbox {...props} field={field} />
default:
return <div>TODO: Insert field options here</div>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
import { useEffect, useMemo } from 'react'
import { Controller, RegisterOptions } from 'react-hook-form'
import { Box, FormControl, Stack } from '@chakra-ui/react'
import { extend, isEmpty, pick } from 'lodash'

import { CheckboxFieldBase } from '~shared/types/field'

import { createBaseValidationRules } from '~utils/fieldValidation'
import FormErrorMessage from '~components/FormControl/FormErrorMessage'
import FormLabel from '~components/FormControl/FormLabel'
import Input from '~components/Input'
import NumberInput from '~components/NumberInput'
import Textarea from '~components/Textarea'
import Toggle from '~components/Toggle'

import { DrawerContentContainer } from './common/DrawerContentContainer'
import { FormFieldDrawerActions } from './common/FormFieldDrawerActions'
import { EditFieldProps } from './common/types'
import { useEditFieldForm } from './common/useEditFieldForm'
import {
SPLIT_TEXTAREA_TRANSFORM,
SPLIT_TEXTAREA_VALIDATION,
} from './common/utils'

type EditCheckboxProps = EditFieldProps<CheckboxFieldBase>

const EDIT_CHECKBOX_FIELD_KEYS = [
'title',
'description',
'required',
'othersRadioButton',
'validateByValue',
] as const

type EditCheckboxKeys = typeof EDIT_CHECKBOX_FIELD_KEYS[number]

type EditCheckboxInputs = Pick<CheckboxFieldBase, EditCheckboxKeys> & {
fieldOptions: string
ValidationOptions: {
customMin?: number | string
customMax?: number | string
}
}

const transformCheckboxFieldToEditForm = (
field: CheckboxFieldBase,
): EditCheckboxInputs => {
const nextValidationOptions = field.validateByValue
? {
customMin: field.ValidationOptions.customMin || '',
customMax: field.ValidationOptions.customMax || '',
}
: { customMin: '', customMax: '' }
return {
...pick(field, EDIT_CHECKBOX_FIELD_KEYS),
fieldOptions: SPLIT_TEXTAREA_TRANSFORM.input(field.fieldOptions),
ValidationOptions: nextValidationOptions,
}
}

const transformCheckboxEditFormToField = (
form: EditCheckboxInputs,
originalField: CheckboxFieldBase,
): CheckboxFieldBase => {
const nextValidationOptions = form.validateByValue
? {
customMin: form.ValidationOptions.customMin || null,
customMax: form.ValidationOptions.customMax || null,
}
: { customMin: null, customMax: null }
return extend({}, originalField, form, {
fieldOptions: SPLIT_TEXTAREA_TRANSFORM.output(form.fieldOptions),
ValidationOptions: nextValidationOptions,
})
}

export const EditCheckbox = (props: EditCheckboxProps): JSX.Element => {
const {
register,
formState: { errors },
isSaveEnabled,
buttonText,
handleUpdateField,
watch,
control,
clearErrors,
} = useEditFieldForm<EditCheckboxInputs, CheckboxFieldBase>({
...props,
transform: {
input: transformCheckboxFieldToEditForm,
output: transformCheckboxEditFormToField,
},
})

const requiredValidationRule = useMemo(
() => createBaseValidationRules({ required: true }),
[],
)

const watchedInputs = watch()

const customMinValidationOptions: RegisterOptions = useMemo(
() => ({
required: {
value:
watchedInputs.validateByValue &&
!watchedInputs.ValidationOptions.customMax,
message: 'Please enter selection limits',
},
min: {
value: 1,
message: 'Cannot be less than 1',
},
validate: {
minLargerThanMax: (val) => {
return (
!val ||
!watchedInputs.validateByValue ||
!watchedInputs.ValidationOptions.customMax ||
Number(val) <= Number(watchedInputs.ValidationOptions.customMax) ||
'Minimum cannot be larger than maximum'
)
},
max: (val) => {
let numOptions = SPLIT_TEXTAREA_TRANSFORM.output(
watchedInputs.fieldOptions,
).length
if (watchedInputs.othersRadioButton) {
numOptions += 1
}
return (
!val || val <= numOptions || 'Cannot be more than number of options'
)
},
},
}),
[watchedInputs],
)

const customMaxValidationOptions: RegisterOptions = useMemo(
() => ({
required: {
value:
watchedInputs.validateByValue &&
!watchedInputs.ValidationOptions.customMin,
message: 'Please enter selection limits',
},
min: {
value: 1,
message: 'Cannot be less than 1',
},
validate: {
maxLargerThanMin: (val) => {
return (
!val ||
!watchedInputs.validateByValue ||
!watchedInputs.ValidationOptions.customMin ||
Number(val) >= Number(watchedInputs.ValidationOptions.customMin) ||
'Maximum cannot be less than minimum'
)
},
max: (val) => {
if (!watchedInputs.validateByValue) return true
let numOptions = SPLIT_TEXTAREA_TRANSFORM.output(
watchedInputs.fieldOptions,
).length
if (watchedInputs.othersRadioButton) {
numOptions += 1
}
return (
!val || val <= numOptions || 'Cannot be more than number of options'
)
},
},
}),
[watchedInputs],
)

// Effect to clear validation option errors when selection limit is toggled off.
useEffect(() => {
if (!watchedInputs.validateByValue) {
clearErrors('ValidationOptions')
}
}, [clearErrors, watchedInputs.validateByValue])
mantariksh marked this conversation as resolved.
Show resolved Hide resolved

return (
<DrawerContentContainer>
<FormControl
isRequired
isReadOnly={props.isLoading}
isInvalid={!!errors.title}
>
<FormLabel>Question</FormLabel>
<Input autoFocus {...register('title', requiredValidationRule)} />
<FormErrorMessage>{errors?.title?.message}</FormErrorMessage>
</FormControl>
<FormControl
isRequired
isReadOnly={props.isLoading}
isInvalid={!!errors.description}
>
<FormLabel>Description</FormLabel>
<Textarea {...register('description')} />
<FormErrorMessage>{errors?.description?.message}</FormErrorMessage>
</FormControl>
<FormControl isReadOnly={props.isLoading}>
<Toggle {...register('required')} label="Required" />
</FormControl>
<FormControl isReadOnly={props.isLoading}>
<Toggle {...register('othersRadioButton')} label="Others" />
</FormControl>
<FormControl
isRequired
isReadOnly={props.isLoading}
isInvalid={!!errors.fieldOptions}
>
<FormLabel>Options</FormLabel>
<Textarea
{...register('fieldOptions', {
validate: SPLIT_TEXTAREA_VALIDATION,
})}
/>
<FormErrorMessage>{errors?.fieldOptions?.message}</FormErrorMessage>
</FormControl>
<Box>
<Toggle
{...register('validateByValue')}
label="Selection limits"
description="Customise the number of options that users are allowed to select"
/>
<FormControl
isDisabled={!watchedInputs.validateByValue}
isReadOnly={props.isLoading}
isInvalid={!isEmpty(errors.ValidationOptions)}
>
<Stack mt="0.5rem" direction="row" spacing="0.5rem">
<Controller
name="ValidationOptions.customMin"
control={control}
rules={customMinValidationOptions}
render={({ field }) => (
<NumberInput
flex={1}
showSteppers={false}
{...field}
placeholder="Minimum"
/>
)}
/>
<Controller
name="ValidationOptions.customMax"
control={control}
rules={customMaxValidationOptions}
render={({ field }) => (
<NumberInput
flex={1}
showSteppers={false}
{...field}
placeholder="Maximum"
/>
)}
/>
</Stack>
<FormErrorMessage>
{errors?.ValidationOptions?.customMin?.message ??
errors?.ValidationOptions?.customMax?.message}
</FormErrorMessage>
</FormControl>
</Box>
<FormFieldDrawerActions
isLoading={props.isLoading}
isSaveEnabled={isSaveEnabled}
buttonText={buttonText}
handleClick={handleUpdateField}
handleCancel={props.handleCancel}
/>
</DrawerContentContainer>
)
}
Loading