diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/BuilderFields.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/BuilderFields.tsx index d156b78ebd..eb7ab1dab5 100644 --- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/BuilderFields.tsx +++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/BuilderFields.tsx @@ -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' @@ -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) => ( - - ))} + {fieldsWithQuestionNos.map((f, i) => { + const activeFieldExtraProps = + f._id === activeFieldId + ? { + isDraggingOver, + fieldBuilderState: stateData.state, + } + : {} + return ( + + ) + })} ) } diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/FieldRow/FieldRowContainer.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/FieldRow/FieldRowContainer.tsx index 90ba9beedc..c8c99ca8ed 100644 --- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/FieldRow/FieldRowContainer.tsx +++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/FieldRow/FieldRowContainer.tsx @@ -55,7 +55,7 @@ import { isMyInfo, } from '~features/myinfo/utils' -import { BuilderAndDesignContextProps } from '../../BuilderAndDesignContext' +import { useBuilderAndDesignContext } from '../../BuilderAndDesignContext' import { setToInactiveSelector as setPaymentToInactiveSelector, usePaymentStore, @@ -68,7 +68,6 @@ import { setStateSelector as setDesignStateSelector, useDesignStore, } from '../../useDesignStore' -import { isDirtySelector, useDirtyFieldStore } from '../../useDirtyFieldStore' import { FieldBuilderState, setToInactiveSelector, @@ -76,7 +75,6 @@ import { useFieldBuilderStore, } from '../../useFieldBuilderStore' import { getAttachmentSizeLimit } from '../../utils/getAttachmentSizeLimit' -import { useDesignColorTheme } from '../../utils/useDesignColorTheme' import { SectionFieldRow } from './SectionFieldRow' import { VerifiableFieldBuilderContainer } from './VerifiableFieldBuilderContainer' @@ -85,11 +83,13 @@ 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 = ({ @@ -97,31 +97,26 @@ const FieldRowContainer = ({ 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 { @@ -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 || @@ -347,52 +296,14 @@ const FieldRowContainer = ({ - - - {isMobile ? ( - } - onClick={handleEditFieldClick} - /> - ) : null} - { - // Fields which are not yet created cannot be duplicated - fieldBuilderState !== FieldBuilderState.CreatingField && ( - - } - /> - - ) - } - - } - onClick={handleDeleteClick} - isLoading={deleteFieldMutation.isLoading} - isDisabled={isAnyMutationLoading} - /> - - - + {isActive && ( + + )} @@ -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 ( + + + {isMobile ? ( + } + onClick={handleEditFieldClick} + /> + ) : null} + { + // Fields which are not yet created cannot be duplicated + fieldBuilderState !== FieldBuilderState.CreatingField && ( + + } + /> + + ) + } + + } + onClick={handleDeleteClick} + isLoading={deleteFieldMutation.isLoading} + isDisabled={isAnyMutationLoading} + /> + + + + ) +} + type FieldRowProps = { field: FormFieldDto colorTheme?: FormColorTheme