Skip to content

Commit

Permalink
feat: add accept deny modal and created field ids from mfb
Browse files Browse the repository at this point in the history
  • Loading branch information
kevin9foong committed Dec 24, 2024
1 parent d65f838 commit 23f0581
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const makeTextPrompt = ({
formId: string
prompt: string
}) => {
return ApiService.post<undefined>(
return ApiService.post<{ message: string; createdFieldIds?: string[] }>(
`${ADMIN_FORM_ENDPOINT}/${formId}/assistance/text-prompt`,
{ prompt },
).then(({ data }) => data)
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/features/admin-form/assistance/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { makeTextPrompt } from '~features/admin-form/assistance/AssistanceServic

import { useToast } from '../../../hooks/useToast'
import { adminFormKeys } from '../common/queries'
import { useMagicFormBuilderStore } from '../create/builder-and-design/useMagicFormBuilderStore'

export const useAssistanceMutations = () => {
const { formId } = useParams()
Expand All @@ -19,7 +20,11 @@ export const useAssistanceMutations = () => {
const useMakeTextPromptMutation = useMutation(
(prompt: string) => makeTextPrompt({ formId, prompt }),
{
onSuccess: () => {
onSuccess: (data) => {
const { createdFieldIds } = data
useMagicFormBuilderStore.setState({
recentlyCreatedFieldIds: new Set(createdFieldIds),
})
queryClient.invalidateQueries(adminFormKeys.id(formId))
toast.closeAll()
toast({ description: 'Form generated successfully', status: 'success' })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {
stateDataSelector,
useFieldBuilderStore,
} from '../useFieldBuilderStore'
import {
recentlyCreatedFieldIdsSelector,
useMagicFormBuilderStore,
} from '../useMagicFormBuilderStore'
import { useDesignColorTheme } from '../utils/useDesignColorTheme'

import FieldRow from './FieldRow'
Expand Down Expand Up @@ -44,6 +48,10 @@ export const BuilderFields = ({

const isDirty = useDirtyFieldStore(isDirtySelector)

const mfbRecentlyCreatedFieldIds = useMagicFormBuilderStore(
recentlyCreatedFieldIdsSelector,
)

return (
<>
{fieldsWithQuestionNos.map((f, i) => {
Expand All @@ -64,6 +72,7 @@ export const BuilderFields = ({
handleBuilderClick={handleBuilderClick}
isDirty={isDirty}
colorTheme={colorTheme}
isHighlighted={mfbRecentlyCreatedFieldIds.has(f._id)}
{...activeFieldExtraProps}
/>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { BiCog, BiDuplicate, BiGridHorizontal, BiTrash } from 'react-icons/bi'
import { useIsMutating } from 'react-query'
import {
Box,
BoxProps,
ButtonGroup,
chakra,
Collapse,
Expand Down Expand Up @@ -94,6 +95,30 @@ export interface FieldRowContainerProps {
colorTheme?: FormColorTheme
// handleBuilderClick is passed down to prevent unnecessary re-renders from useContext
handleBuilderClick: CreatePageSidebarContextProps['handleBuilderClick']
isHighlighted?: boolean
}

/**
* Used for highlighting fields that were created by Magic Form Builder.
*/
const HighlightableBox = ({
children,
isHighlighted,
...props
}: {
children: React.ReactNode
isHighlighted?: boolean
} & BoxProps) => {
return (
<Box
borderColor="danger.300"
borderRadius="0.25rem"
borderWidth={isHighlighted ? '0.125rem' : '0'}
{...props}
>
{children}
</Box>
)
}

const FieldRowContainer = ({
Expand All @@ -106,6 +131,7 @@ const FieldRowContainer = ({
isDirty,
colorTheme,
handleBuilderClick,
isHighlighted,
}: FieldRowContainerProps): JSX.Element => {
const isMobile = useIsMobile()
const numFormFieldMutations = useIsMutating(adminFormKeys.base)
Expand Down Expand Up @@ -285,7 +311,8 @@ const FieldRowContainer = ({
)}
</chakra.button>
</Fade>
<Box
<HighlightableBox
isHighlighted={isHighlighted}
px={{ base: '0.75rem', md: '1.5rem' }}
pb={{ base: '0.75rem', md: '1.5rem' }}
w="100%"
Expand All @@ -300,7 +327,7 @@ const FieldRowContainer = ({
showMyInfoBadge={isMyInfoField}
/>
</FormProvider>
</Box>
</HighlightableBox>
<Collapse in={isActive} style={{ width: '100%' }}>
{isActive && (
<FieldButtonGroup
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { BiSolidMagicWand } from 'react-icons/bi'
import { BiSolidMagicWand, BiTrash } from 'react-icons/bi'
import { HiSparkles } from 'react-icons/hi2'
import {
Button,
Expand All @@ -20,11 +20,15 @@ import {
Tooltip,
} from '@chakra-ui/react'

import { BxCheck } from '~/assets/icons'

import { useIsMobile } from '~hooks/useIsMobile'
import { FormErrorMessage } from '~components/FormControl/FormErrorMessage/FormErrorMessage'

import { useAssistanceMutations } from '~features/admin-form/assistance/mutations'

import { useMagicFormBuilderStore } from '../../useMagicFormBuilderStore'

const GENERATE_FORM_PLACEHOLDER =
'Describe form, fields and sections to create...'

Expand Down Expand Up @@ -61,16 +65,12 @@ const MagicFormBuilderButton = ({
)
}

interface TextPromptInputs {
prompt: string
}

const MagicFormBuilderPopover = ({
isOpen,
setIsOpen,
const MagicFormBuilderCreateFormPrompt = ({
onSettled,
onClose,
}: {
isOpen: boolean
setIsOpen: (isOpen: boolean) => void
onSettled: () => void
onClose: () => void
}) => {
const {
register,
Expand All @@ -82,50 +82,130 @@ const MagicFormBuilderPopover = ({

const onSubmit = async ({ prompt }: TextPromptInputs) => {
useMakeTextPromptMutation.mutate(prompt, {
onSettled: () => setIsOpen(false),
onSettled: onSettled,
})
}

return (
<Popover placement="right" isOpen={isOpen}>
<form onSubmit={handleSubmit(onSubmit)}>
<PopoverHeader>
<Text textStyle="h4">Generate form</Text>
</PopoverHeader>
<PopoverCloseButton onClick={onClose} />
<PopoverBody>
<FormControl isRequired isInvalid={false}>
<Textarea
placeholder={GENERATE_FORM_PLACEHOLDER}
{...register('prompt', {
required: 'Please enter a prompt.',
maxLength: {
value: 30,
message: 'Please enter at most 500 characters.',
},
})}
/>
<FormErrorMessage>{errors.prompt?.message}</FormErrorMessage>
</FormControl>
</PopoverBody>
<PopoverFooter>
<Flex justifyContent="flex-end">
<Button
leftIcon={<HiSparkles fontSize="1.5rem" />}
type="submit"
isLoading={useMakeTextPromptMutation.isLoading}
>
Generate
</Button>
</Flex>
</PopoverFooter>
</form>
)
}

const MagicFormBuilderAcceptDeny = ({
onAccept,
onDeny,
onClose,
}: {
onAccept: () => void
onDeny: () => void
onClose: () => void
}) => {
return (
<>
<PopoverHeader>
<Text textStyle="h4">Keep these changes?</Text>
</PopoverHeader>
<PopoverCloseButton onClick={onClose} />
<PopoverBody>
<Flex direction="column" gap="0.25rem">
<Button
leftIcon={<BxCheck />}
variant="solid"
onClick={onAccept}
colorScheme="success"
>
Accept
</Button>
<Button
leftIcon={<BiTrash />}
variant="solid"
onClick={onDeny}
colorScheme="danger"
>
Deny
</Button>
</Flex>
</PopoverBody>
</>
)
}

interface TextPromptInputs {
prompt: string
}

const MagicFormBuilderPopover = ({
isOpen,
setIsOpen,
}: {
isOpen: boolean
setIsOpen: (isOpen: boolean) => void
}) => {
const [isAcceptDenyOpen, setIsAcceptDenyOpen] = useState(false)
const clearRecentlyCreatedFieldIds = useMagicFormBuilderStore(
(state) => state.clearRecentlyCreatedFieldIds,
)

const onClickDefaults = () => {
clearRecentlyCreatedFieldIds()
setIsOpen(false)
setTimeout(() => setIsAcceptDenyOpen(false), 100) // delay to allow popover to close before updating state
}

return (
<Popover isLazy placement="right" isOpen={isOpen}>
<PopoverAnchor>
<MagicFormBuilderButton onClick={() => setIsOpen(!isOpen)} />
</PopoverAnchor>
<Portal>
{/* TODO: (MFBv1.1) Fix the position of the popover. */}
<PopoverContent bg="white" top="15vh" left="40vw">
<form onSubmit={handleSubmit(onSubmit)}>
<PopoverHeader>
<Text textStyle="h4">Generate form</Text>
</PopoverHeader>
<PopoverCloseButton onClick={() => setIsOpen(false)} />
<PopoverBody>
<FormControl isRequired isInvalid={false}>
<Textarea
placeholder={GENERATE_FORM_PLACEHOLDER}
{...register('prompt', {
required: 'Please enter a prompt.',
maxLength: {
value: 30,
message: 'Please enter at most 500 characters.',
},
})}
/>
<FormErrorMessage>{errors.prompt?.message}</FormErrorMessage>
</FormControl>
</PopoverBody>
<PopoverFooter>
<Flex justifyContent="flex-end">
<Button
leftIcon={<HiSparkles fontSize="1.5rem" />}
type="submit"
isLoading={useMakeTextPromptMutation.isLoading}
>
Generate
</Button>
</Flex>
</PopoverFooter>
</form>
{!isAcceptDenyOpen ? (
<MagicFormBuilderCreateFormPrompt
onClose={() => setIsOpen(false)}
onSettled={() => setIsAcceptDenyOpen(true)}
/>
) : (
<MagicFormBuilderAcceptDeny
onAccept={onClickDefaults}
onDeny={() => {
// trigger deletion of fields with field ids in recentlyCreatedFieldIds
onClickDefaults()
}}
onClose={() => setIsOpen(false)}
/>
)}
</PopoverContent>
</Portal>
</Popover>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import create from 'zustand'

export type MagicFormBuilderStore = {
recentlyCreatedFieldIds: Set<string>
setRecentlyCreatedFieldIds: (fieldIds: Set<string>) => void
clearRecentlyCreatedFieldIds: () => void
}

export const useMagicFormBuilderStore = create<MagicFormBuilderStore>(
(set) => ({
recentlyCreatedFieldIds: new Set(),
setRecentlyCreatedFieldIds: (fieldIds) =>
set({ recentlyCreatedFieldIds: fieldIds }),
clearRecentlyCreatedFieldIds: () =>
set({ recentlyCreatedFieldIds: new Set() }),
}),
)

export const recentlyCreatedFieldIdsSelector = (
state: MagicFormBuilderStore,
): MagicFormBuilderStore['recentlyCreatedFieldIds'] =>
state.recentlyCreatedFieldIds
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ interface ITextPrompt {

const _handleTextPrompt: ControllerHandler<
{ formId: string },
{ message: string },
{ message: string; createdFieldIds?: string[] },
ITextPrompt
> = async (req, res) => {
const { formId } = req.params
Expand All @@ -57,9 +57,10 @@ const _handleTextPrompt: ControllerHandler<
userPrompt: req.body.prompt,
}),
)
.map(() =>
.map((createdFieldIds) =>
res.status(StatusCodes.OK).json({
message: 'Created form fields using text prompt successfully.',
createdFieldIds: createdFieldIds.map((field) => field._id.toString()),
}),
)
.mapErr((error) => {
Expand Down
Loading

0 comments on commit 23f0581

Please sign in to comment.