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

perf: conditionally use hooks based on activeness of field row #6303

Merged
merged 8 commits into from
May 15, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { useCreatePageSidebar } from '~features/admin-form/create/common/CreateP
import { augmentWithQuestionNo } from '~features/form/utils'
import { FieldIdSet } from '~features/logic/types'

import { useBuilderAndDesignContext } from '../BuilderAndDesignContext'
import { PENDING_CREATE_FIELD_ID } from '../constants'
import { isDirtySelector, useDirtyFieldStore } from '../useDirtyFieldStore'
import {
FieldBuilderState,
stateDataSelector,
useFieldBuilderStore,
} from '../useFieldBuilderStore'
import { useDesignColorTheme } from '../utils/useDesignColorTheme'

import FieldRow from './FieldRow'

Expand All @@ -29,31 +30,41 @@ export const BuilderFields = ({
const stateData = useFieldBuilderStore(stateDataSelector)

const { handleBuilderClick } = useCreatePageSidebar()
const {
deleteFieldModalDisclosure: { onOpen: onDeleteModalOpen },
} = useBuilderAndDesignContext()

const activeFieldId =
stateData.state === FieldBuilderState.EditingField
? stateData.field._id
: stateData.state === FieldBuilderState.CreatingField
? PENDING_CREATE_FIELD_ID
: null

const colorTheme = useDesignColorTheme()

const isDirty = useDirtyFieldStore(isDirtySelector)

return (
<>
{fieldsWithQuestionNos.map((f, i) => (
<FieldRow
index={i}
key={f._id}
field={f}
isHiddenByLogic={!visibleFieldIds.has(f._id)}
isDraggingOver={isDraggingOver}
isActive={
stateData.state === FieldBuilderState.EditingField
? f._id === stateData.field._id
: stateData.state === FieldBuilderState.CreatingField
? f._id === PENDING_CREATE_FIELD_ID
: false
}
fieldBuilderState={stateData.state}
handleBuilderClick={handleBuilderClick}
onDeleteModalOpen={onDeleteModalOpen}
/>
))}
{fieldsWithQuestionNos.map((f, i) => {
const activeFieldExtraProps =
f._id === activeFieldId
? {
isDraggingOver,
fieldBuilderState: stateData.state,
}
: {}
return (
<FieldRow
index={i}
key={f._id}
field={f}
isHiddenByLogic={!visibleFieldIds.has(f._id)}
handleBuilderClick={handleBuilderClick}
isDirty={isDirty}
colorTheme={colorTheme}
{...activeFieldExtraProps}
/>
)
})}
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import {
isMyInfo,
} from '~features/myinfo/utils'

import { BuilderAndDesignContextProps } from '../../BuilderAndDesignContext'
import { useBuilderAndDesignContext } from '../../BuilderAndDesignContext'
import {
setToInactiveSelector as setPaymentToInactiveSelector,
usePaymentStore,
Expand All @@ -68,15 +68,13 @@ import {
setStateSelector as setDesignStateSelector,
useDesignStore,
} from '../../useDesignStore'
import { isDirtySelector, useDirtyFieldStore } from '../../useDirtyFieldStore'
import {
FieldBuilderState,
setToInactiveSelector,
updateEditStateSelector,
useFieldBuilderStore,
} from '../../useFieldBuilderStore'
import { getAttachmentSizeLimit } from '../../utils/getAttachmentSizeLimit'
import { useDesignColorTheme } from '../../utils/useDesignColorTheme'

import { SectionFieldRow } from './SectionFieldRow'
import { VerifiableFieldBuilderContainer } from './VerifiableFieldBuilderContainer'
Expand All @@ -85,43 +83,40 @@ export interface FieldRowContainerProps {
field: FormFieldDto
index: number
isHiddenByLogic: boolean
isDraggingOver: boolean
isActive: boolean
fieldBuilderState: FieldBuilderState
isDraggingOver?: boolean
// Field only needs to know fieldBuilderState if it is active, else it is agnostic to state
fieldBuilderState?: FieldBuilderState
isDirty: boolean
colorTheme?: FormColorTheme
// handleBuilderClick is passed down to prevent unnecessary re-renders from useContext
handleBuilderClick: CreatePageSidebarContextProps['handleBuilderClick']
onDeleteModalOpen: BuilderAndDesignContextProps['deleteFieldModalDisclosure']['onOpen']
}

const FieldRowContainer = ({
field,
index,
isHiddenByLogic,
isDraggingOver,
isActive,
fieldBuilderState,
isDirty,
colorTheme,
handleBuilderClick,
onDeleteModalOpen,
}: FieldRowContainerProps): JSX.Element => {
const isMobile = useIsMobile()
const { data: form } = useCreateTabForm()
const numFormFieldMutations = useIsMutating(adminFormKeys.base)
const setToInactive = useFieldBuilderStore(setToInactiveSelector)
const updateEditState = useFieldBuilderStore(updateEditStateSelector)

const isDirty = useDirtyFieldStore(isDirtySelector)
const toast = useToast({ status: 'danger', isClosable: true })

const setDesignState = useDesignStore(setDesignStateSelector)
const setPaymentStateToInactive = usePaymentStore(
setPaymentToInactiveSelector,
)
const { duplicateFieldMutation } = useDuplicateFormField()
const { deleteFieldMutation } = useDeleteFormField()

const colorTheme = useDesignColorTheme()

const isMyInfoField = useMemo(() => isMyInfo(field), [field])

// Explicitly defining isActive here to prevent constant checks to undefined
// due to falsy nature of FieldBuilderState.CreatingField = 0
const isActive = fieldBuilderState !== undefined

const defaultFieldValues = useMemo(() => {
if (field.fieldType === BasicField.Table) {
return {
Expand Down Expand Up @@ -191,52 +186,6 @@ const FieldRowContainer = ({
[handleFieldClick],
)

const handleEditFieldClick = useCallback(() => {
if (isMobile) {
handleBuilderClick(false)
}
}, [handleBuilderClick, isMobile])

const handleDuplicateClick = useCallback(() => {
if (!form) return
// Duplicate button should be hidden if field is not yet created, but guard here just in case
if (fieldBuilderState === FieldBuilderState.CreatingField) return
// Disallow duplicating attachment fields if after the dupe, the filesize exceeds the limit
if (field.fieldType === BasicField.Attachment) {
const existingAttachmentsSize = form.form_fields.reduce(
(sum, ff) =>
ff.fieldType === BasicField.Attachment
? sum + Number(ff.attachmentSize)
: sum,
0,
)
const remainingAvailableSize =
getAttachmentSizeLimit(form.responseMode) - existingAttachmentsSize
const thisAttachmentSize = Number(field.attachmentSize)
if (thisAttachmentSize > remainingAvailableSize) {
toast({
useMarkdown: true,
description: `The field "${field.title}" could not be duplicated. The attachment size of **${thisAttachmentSize} MB** exceeds the form's remaining available attachment size of **${remainingAvailableSize} MB**.`,
})
return
}
}
duplicateFieldMutation.mutate(field._id)
}, [fieldBuilderState, field, duplicateFieldMutation, form, toast])

const handleDeleteClick = useCallback(() => {
if (fieldBuilderState === FieldBuilderState.CreatingField) {
setToInactive()
} else if (fieldBuilderState === FieldBuilderState.EditingField) {
onDeleteModalOpen()
}
}, [setToInactive, fieldBuilderState, onDeleteModalOpen])

const isAnyMutationLoading = useMemo(
() => duplicateFieldMutation.isLoading || deleteFieldMutation.isLoading,
[duplicateFieldMutation, deleteFieldMutation],
)

const isDragDisabled = useMemo(() => {
return (
!isActive ||
Expand Down Expand Up @@ -347,52 +296,14 @@ const FieldRowContainer = ({
</FormProvider>
</Box>
<Collapse in={isActive} style={{ width: '100%' }}>
<Flex
px={{ base: '0.75rem', md: '1.5rem' }}
flex={1}
borderTop="1px solid var(--chakra-colors-neutral-300)"
justify="flex-end"
>
<ButtonGroup
variant="clear"
colorScheme="secondary"
spacing={0}
>
{isMobile ? (
<IconButton
variant="clear"
colorScheme="secondary"
aria-label="Edit field"
icon={<BiCog fontSize="1.25rem" />}
onClick={handleEditFieldClick}
/>
) : null}
{
// Fields which are not yet created cannot be duplicated
fieldBuilderState !== FieldBuilderState.CreatingField && (
<Tooltip label="Duplicate field">
<IconButton
aria-label="Duplicate field"
isDisabled={isAnyMutationLoading}
onClick={handleDuplicateClick}
isLoading={duplicateFieldMutation.isLoading}
icon={<BiDuplicate fontSize="1.25rem" />}
/>
</Tooltip>
)
}
<Tooltip label="Delete field">
<IconButton
colorScheme="danger"
aria-label="Delete field"
icon={<BiTrash fontSize="1.25rem" />}
onClick={handleDeleteClick}
isLoading={deleteFieldMutation.isLoading}
isDisabled={isAnyMutationLoading}
/>
</Tooltip>
</ButtonGroup>
</Flex>
{isActive && (
<FieldButtonGroup
timotheeg marked this conversation as resolved.
Show resolved Hide resolved
field={field}
fieldBuilderState={fieldBuilderState}
isMobile={isMobile}
handleBuilderClick={handleBuilderClick}
/>
)}
</Collapse>
</Flex>
</Tooltip>
Expand All @@ -406,6 +317,124 @@ export const MemoizedFieldRow = memo(FieldRowContainer, (prevProps, newProps) =>
isEqual(prevProps, newProps),
)

type FieldButtonGroupProps = {
field: FormFieldDto
fieldBuilderState: FieldBuilderState
isMobile: boolean
handleBuilderClick: CreatePageSidebarContextProps['handleBuilderClick']
}

const FieldButtonGroup = ({
field,
fieldBuilderState,
isMobile,
handleBuilderClick,
}: FieldButtonGroupProps) => {
const setToInactive = useFieldBuilderStore(setToInactiveSelector)

const { data: form } = useCreateTabForm()

const toast = useToast({ status: 'danger', isClosable: true })

const { duplicateFieldMutation } = useDuplicateFormField()
const { deleteFieldMutation } = useDeleteFormField()
const {
deleteFieldModalDisclosure: { onOpen: onDeleteModalOpen },
} = useBuilderAndDesignContext()

const handleEditFieldClick = useCallback(() => {
if (isMobile) {
handleBuilderClick(false)
}
}, [handleBuilderClick, isMobile])

const isAnyMutationLoading = useMemo(
() => duplicateFieldMutation.isLoading || deleteFieldMutation.isLoading,
[duplicateFieldMutation, deleteFieldMutation],
)
const handleDuplicateClick = useCallback(() => {
// Duplicate button should be hidden if field is not yet created, but guard here just in case
if (fieldBuilderState === FieldBuilderState.CreatingField) return
// Disallow duplicating attachment fields if after the dupe, the filesize exceeds the limit

if (field.fieldType === BasicField.Attachment) {
// Get remaining available attachment size limit
const availableAttachmentSize = form
? getAttachmentSizeLimit(form.responseMode) -
form.form_fields.reduce(
(sum, ff) =>
ff.fieldType === BasicField.Attachment
? sum + Number(ff.attachmentSize)
: sum,
0,
)
: 0
const thisAttachmentSize = Number(field.attachmentSize)
if (thisAttachmentSize > availableAttachmentSize) {
toast({
useMarkdown: true,
description: `The field "${field.title}" could not be duplicated. The attachment size of **${thisAttachmentSize} MB** exceeds the form's remaining available attachment size of **${availableAttachmentSize} MB**.`,
})
return
}
}
duplicateFieldMutation.mutate(field._id)
}, [form, fieldBuilderState, field, duplicateFieldMutation, toast])

const handleDeleteClick = useCallback(() => {
if (fieldBuilderState === FieldBuilderState.CreatingField) {
setToInactive()
} else if (fieldBuilderState === FieldBuilderState.EditingField) {
onDeleteModalOpen()
}
}, [setToInactive, fieldBuilderState, onDeleteModalOpen])

return (
<Flex
px={{ base: '0.75rem', md: '1.5rem' }}
flex={1}
borderTop="1px solid var(--chakra-colors-neutral-300)"
justify="flex-end"
>
<ButtonGroup variant="clear" colorScheme="secondary" spacing={0}>
{isMobile ? (
<IconButton
variant="clear"
colorScheme="secondary"
aria-label="Edit field"
icon={<BiCog fontSize="1.25rem" />}
onClick={handleEditFieldClick}
/>
) : null}
{
// Fields which are not yet created cannot be duplicated
fieldBuilderState !== FieldBuilderState.CreatingField && (
<Tooltip label="Duplicate field">
<IconButton
aria-label="Duplicate field"
isDisabled={isAnyMutationLoading}
onClick={handleDuplicateClick}
isLoading={duplicateFieldMutation.isLoading}
icon={<BiDuplicate fontSize="1.25rem" />}
/>
</Tooltip>
)
}
<Tooltip label="Delete field">
<IconButton
colorScheme="danger"
aria-label="Delete field"
icon={<BiTrash fontSize="1.25rem" />}
onClick={handleDeleteClick}
isLoading={deleteFieldMutation.isLoading}
isDisabled={isAnyMutationLoading}
/>
</Tooltip>
</ButtonGroup>
</Flex>
)
}

type FieldRowProps = {
field: FormFieldDto
colorTheme?: FormColorTheme
Expand Down