Skip to content

Commit

Permalink
perf: conditionally use hooks based on activeness of field row (#6303)
Browse files Browse the repository at this point in the history
* perf: refactor active field button group

which separates the hooks relating to handling duplicate fields and deleting fields. Preventing unnecessary re-renders to non-active fields.

* perf: update parent builder field

* perf: update dragging over flag only for active fields

* refactor: available size limit should only be computed on attachment

* chore: remove unnecessary check

* refactor: improve name of activeField

* refactor: improve readability for dragging over

* refactor: only pass extra props when active
  • Loading branch information
foochifa authored May 15, 2023
1 parent 7f16e44 commit 1c33d4e
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 133 deletions.
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
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

0 comments on commit 1c33d4e

Please sign in to comment.