Skip to content

Commit

Permalink
feat(mrf): conditional routing (#7804)
Browse files Browse the repository at this point in the history
* feat: add cond routing type and modal to workflow builder

* feat: add step 2 modal content

* feat: add step 3 modal content

* feat: add attachment box under radio button and fix modal interaction

* feat: allow save conditional workflow step

* feat: allow download of csv edit file

* feat: set up fe component

* feat: store optionsToFieldsMap in dropdown

* feat: add be email routing for conditional routing

* feat: move conditional field to main form and update modal to 2 step

* feat: add be routing for conditional routing

* feat: add spacing for cond routing, fix inactive state undefined error

* feat: create endpoint for updating dropdown mapping

* feat: update attachment state when singleselect changes

* feat: add FE validation for CSV

* feat: add validation at the workflow block

* feat: update positioning of error message, modal copy and update fe cache

* feat: use state instead of using the same form state

* feat: implement download csv

* feat: add error message to inactive step block

* feat: add warning text to dropdown

* fix: build errors

* feat: clear error on button press and update error copy

* feat: conditionally show dropdown edit warning if cond routing

* fix: validation of options

* feat: add warning in inactive step block for mismatched options

* feat: add be validation for mapping

* chore: remove stray console log

* chore: remove step number from modal

* chore: update error message copy

* feat: add tc for validation

* feat: add tc for conditional fetch email

* feat: add tc for updating options mapping

* chore: remove unused import

* chore: add beta flag for cond routing

* fix: move isDisabled to radiogroup to bypass undefined re-render issue

* feat: add tc for edit step block

* feat: active and inactive step block chromatic stories

* feat: edit dropdown stories

* feat: add stories for conditional routing option modal

* chore: expose subroutine errors

* chore: add field validation test

* chore: remove unused prop

* fix: reduce usage of watch to getValues, refactor variables

* chore: move conditional-routing-example.png to the same folder as usage for colocation

* fix: remove renderHook from stories which is not supported

* fix: import image directly instead of using path

* chore: refactor respondent block into smaller components

* feat: move csv template parsing for header to common function

* feat: add rationale for attaching mapping to field instead of step

* chore: remove multiple string duplicates by shifting into getFileName function

* feat: add trimming of email

* feat: add trimming of email during validation

* fix: display update error if no mapping for selected cond field

* fix: cannot change respondent type bug

* chore: add info logs if no target emails found

* fix: remove word template from modal

* fix: copy changes and bugs

* feat: disable modal step 2 before csv template downloaded

* fix: chromatic stories to reflect new changes

* fix: prettier formatting

* fix: add . to error message

---------

Co-authored-by: Ken <[email protected]>
  • Loading branch information
kevin9foong and KenLSM authored Nov 26, 2024
1 parent f15acda commit 07a5197
Show file tree
Hide file tree
Showing 51 changed files with 2,804 additions and 298 deletions.
55 changes: 55 additions & 0 deletions frontend/src/components/Button/NextAndBackButtonGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Stack } from '@chakra-ui/react'

import { useIsMobile } from '~hooks/useIsMobile'

import { Button } from './Button'

interface NextAndBackButtonProps {
handleBack: () => void
handleNext: () => void
nextButtonLabel?: string
backButtonLabel?: string
isNextDisabled?: boolean
isBackDisabled?: boolean
nextButtonColorScheme?: 'danger'
}

export const NextAndBackButtonGroup = ({
handleBack,
handleNext,
nextButtonLabel = 'Next',
backButtonLabel = 'Back',
isNextDisabled = false,
isBackDisabled = false,
nextButtonColorScheme,
}: NextAndBackButtonProps): JSX.Element => {
const isMobile = useIsMobile()

return (
<Stack
justify="flex-start"
align="center"
spacing="1rem"
direction={{ base: 'column', md: 'row-reverse' }}
w="100%"
>
<Button
colorScheme={nextButtonColorScheme}
isDisabled={isNextDisabled}
onClick={handleNext}
isFullWidth={isMobile}
>
{nextButtonLabel}
</Button>
<Button
variant="clear"
colorScheme="secondary"
isDisabled={isBackDisabled}
onClick={handleBack}
isFullWidth={isMobile}
>
{backButtonLabel}
</Button>
</Stack>
)
}
1 change: 1 addition & 0 deletions frontend/src/components/Button/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export type { ButtonProps } from './Button'
export { Button as default } from './Button'
export * from './NextAndBackButtonGroup'
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
import { Meta, StoryFn } from '@storybook/react'

import { BasicField, DropdownFieldBase } from '~shared/types'
import {
BasicField,
DropdownFieldBase,
FormFieldDto,
FormResponseMode,
FormWorkflowStepDto,
WorkflowType,
} from '~shared/types'

import { createFormBuilderMocks } from '~/mocks/msw/handlers/admin-form'

import { EditFieldDrawerDecorator, StoryRouter } from '~utils/storybook'

import { EditDropdown } from './EditDropdown'

const DEFAULT_DROPDOWN_FIELD: DropdownFieldBase = {
const DEFAULT_DROPDOWN_FIELD: DropdownFieldBase & {
_id: FormFieldDto['_id']
} = {
title: 'Storybook Dropdown',
description: 'Some description about Dropdown',
required: true,
disabled: false,
fieldType: BasicField.Dropdown,
fieldOptions: ['Option 1', 'Option 2', 'Option 3'],
globalId: 'unused',
_id: 'dropdown_field_id',
}

export default {
Expand All @@ -39,7 +49,9 @@ export default {
} as Meta<StoryArgs>

interface StoryArgs {
field: DropdownFieldBase
field: DropdownFieldBase & {
_id: FormFieldDto['_id']
}
}

const Template: StoryFn<StoryArgs> = ({ field }) => {
Expand All @@ -48,3 +60,28 @@ const Template: StoryFn<StoryArgs> = ({ field }) => {

export const Default = Template.bind({})
Default.storyName = 'EditDropdown'

const workflow_step_1: FormWorkflowStepDto = {
_id: '61e6857c9c794b0012f1c6f8',
workflow_type: WorkflowType.Static,
emails: [],
edit: [],
}

const workflow_step_2: FormWorkflowStepDto = {
_id: '61e6857c9c794b0012f1c6f8',
workflow_type: WorkflowType.Conditional,
conditional_field: DEFAULT_DROPDOWN_FIELD._id,
edit: [],
}

export const FieldUsedForConditionalRouting = Template.bind({})
FieldUsedForConditionalRouting.parameters = {
msw: createFormBuilderMocks(
{
responseMode: FormResponseMode.Multirespondent,
workflow: [workflow_step_1, workflow_step_2],
},
0,
),
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ import { useTranslation } from 'react-i18next'
import { FormControl } from '@chakra-ui/react'
import { extend, pick } from 'lodash'

import { WorkflowType } from '~shared/types'
import { DropdownFieldBase } from '~shared/types/field'

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

import { useAdminFormWorkflow } from '../../../../../../create/workflow/hooks/useAdminFormWorkflow'
import { CreatePageDrawerContentContainer } from '../../../../../common'
import {
SPLIT_TEXTAREA_TRANSFORM,
Expand Down Expand Up @@ -51,6 +54,7 @@ const transformDropdownEditFormToField = (

export const EditDropdown = ({ field }: EditDropdownProps): JSX.Element => {
const { t } = useTranslation()
const { formWorkflow } = useAdminFormWorkflow()
const {
register,
formState: { errors },
Expand All @@ -72,6 +76,14 @@ export const EditDropdown = ({ field }: EditDropdownProps): JSX.Element => {
[],
)

const isFieldUsedForConditionalRouting = useMemo(() => {
return formWorkflow?.some(
(workflow) =>
workflow.workflow_type === WorkflowType.Conditional &&
workflow.conditional_field === field._id,
)
}, [formWorkflow, field._id])

return (
<CreatePageDrawerContentContainer>
<FormControl isRequired isReadOnly={isLoading} isInvalid={!!errors.title}>
Expand Down Expand Up @@ -106,6 +118,13 @@ export const EditDropdown = ({ field }: EditDropdownProps): JSX.Element => {
<FormErrorMessage>
{errors?.fieldOptionsString?.message}
</FormErrorMessage>
{isFieldUsedForConditionalRouting ? (
<InlineMessage mt="1rem" variant="warning">
This field is linked to a step in your workflow. If you make any
changes, ensure that the options and emails are updated in your CSV
file.
</InlineMessage>
) : null}
</FormControl>
<FormFieldDrawerActions
isLoading={isLoading}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { FieldCreateDto, FormFieldDto } from '~shared/types/field'
import {
DropdownFieldBase,
FieldCreateDto,
FormFieldDto,
} from '~shared/types/field'

import { transformAllIsoStringsToDate } from '~utils/date'
import { ApiService } from '~services/ApiService'
Expand Down Expand Up @@ -45,6 +49,23 @@ export const updateSingleFormField = async ({
.then(transformAllIsoStringsToDate)
}

export const updateOptionsToRecipientsMap = async ({
formId,
fieldId,
optionsToRecipientsMap,
}: {
formId: string
fieldId: string
optionsToRecipientsMap: DropdownFieldBase['optionsToRecipientsMap']
}): Promise<FormFieldDto> => {
return ApiService.put<FormFieldDto>(
`${ADMIN_FORM_ENDPOINT}/${formId}/fields/${fieldId}/options-to-recipients-map`,
{ optionsToRecipientsMap },
)
.then(({ data }) => data)
.then(transformAllIsoStringsToDate)
}

/**
* Reorders the field to the given new position.
* @param formId the id of the form to perform the field reorder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import { useTranslation } from 'react-i18next'
import { useMutation, useQueryClient } from 'react-query'
import { useParams } from 'react-router-dom'

import { FormFieldDto } from '~shared/types/field'
import { DropdownFieldBase, FormFieldDto } from '~shared/types/field'
import { AdminFormDto } from '~shared/types/form'

import { useToast } from '~hooks/useToast'

import { adminFormKeys } from '~features/admin-form/common/queries'

import { updateSingleFormField } from '../UpdateFormFieldService'
import {
updateOptionsToRecipientsMap,
updateSingleFormField,
} from '../UpdateFormFieldService'
import {
FieldBuilderState,
fieldBuilderStateSelector,
Expand Down Expand Up @@ -81,5 +84,39 @@ export const useEditFormField = () => {
onError: handleError,
},
),
editOptionToRecipientsMutation: useMutation(
({
fieldId,
optionsToRecipientsMap,
}: {
fieldId: string
optionsToRecipientsMap: DropdownFieldBase['optionsToRecipientsMap']
}) => {
return updateOptionsToRecipientsMap({
formId,
fieldId,
optionsToRecipientsMap,
})
},
{
onSuccess: (newField: FormFieldDto) => {
toast.closeAll()
toast({
description: 'Your options and emails have been saved.',
})
queryClient.setQueryData<AdminFormDto>(adminFormKey, (oldForm) => {
// Should not happen, should not be able to update field if there is no
// existing data.
if (!oldForm) throw new Error('Query should have been set')
const currentFieldIndex = oldForm.form_fields.findIndex(
(ff) => ff._id === newField._id,
)
oldForm.form_fields[currentFieldIndex] = newField
return oldForm
})
},
onError: handleError,
},
),
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ export const DeleteLogicModal = ({
<ModalHeader color="secondary.700">Delete logic</ModalHeader>
<ModalBody whiteSpace="pre-wrap">
<Text textStyle="body-2" color="secondary.500">
Are you sure you want to delete this logic? This action is not
reversible.
Are you sure you want to delete this logic? This action cannot be
undone.
</Text>
</ModalBody>
<ModalFooter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const FieldLogicBadge = ({
field,
defaults = {
variant: 'error',
message: 'This field was deleted and has been removed from your workflow',
message: 'This field was deleted, please select another field',
},
}: FieldLogicBadgeProps) => {
const fieldMeta = useMemo(
Expand Down
Loading

0 comments on commit 07a5197

Please sign in to comment.