2
0

feat(inputs): Add Condition step

This commit is contained in:
Baptiste Arnaud
2022-01-15 17:30:20 +01:00
parent 4ccb7bca49
commit 2814a352b2
30 changed files with 1178 additions and 243 deletions

View File

@ -1,7 +1,13 @@
import { PopoverContent, PopoverArrow, PopoverBody } from '@chakra-ui/react'
import {
PopoverContent,
PopoverArrow,
PopoverBody,
useEventListener,
} from '@chakra-ui/react'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import {
ChoiceInputOptions,
ConditionOptions,
InputStep,
InputStepType,
LogicStepType,
@ -9,6 +15,7 @@ import {
Step,
TextInputOptions,
} from 'models'
import { useRef } from 'react'
import {
TextInputSettingsBody,
NumberInputSettingsBody,
@ -17,6 +24,7 @@ import {
DateInputSettingsBody,
} from './bodies'
import { ChoiceInputSettingsBody } from './bodies/ChoiceInputSettingsBody'
import { ConditionSettingsBody } from './bodies/ConditionSettingsBody'
import { PhoneNumberSettingsBody } from './bodies/PhoneNumberSettingsBody'
import { SetVariableSettingsBody } from './bodies/SetVariableSettingsBody'
@ -25,12 +33,17 @@ type Props = {
}
export const SettingsPopoverContent = ({ step }: Props) => {
const ref = useRef<HTMLDivElement | null>(null)
const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation()
const handleMouseWheel = (e: WheelEvent) => {
e.stopPropagation()
}
useEventListener('wheel', handleMouseWheel, ref.current)
return (
<PopoverContent onMouseDown={handleMouseDown}>
<PopoverArrow />
<PopoverBody p="6">
<PopoverBody p="6" overflowY="scroll" maxH="400px" ref={ref}>
<SettingsPopoverBodyContent step={step} />
</PopoverBody>
</PopoverContent>
@ -40,7 +53,11 @@ export const SettingsPopoverContent = ({ step }: Props) => {
const SettingsPopoverBodyContent = ({ step }: Props) => {
const { updateStep } = useTypebot()
const handleOptionsChange = (
options: TextInputOptions | ChoiceInputOptions | SetVariableOptions
options:
| TextInputOptions
| ChoiceInputOptions
| SetVariableOptions
| ConditionOptions
) => updateStep(step.id, { options } as Partial<InputStep>)
switch (step.type) {
@ -108,6 +125,14 @@ const SettingsPopoverBodyContent = ({ step }: Props) => {
/>
)
}
case LogicStepType.CONDITION: {
return (
<ConditionSettingsBody
options={step.options}
onOptionsChange={handleOptionsChange}
/>
)
}
default: {
return <></>
}

View File

@ -0,0 +1,157 @@
import { Button, Fade, Flex, IconButton, Stack } from '@chakra-ui/react'
import { PlusIcon, TrashIcon } from 'assets/icons'
import { DebouncedInput } from 'components/shared/DebouncedInput'
import { DropdownList } from 'components/shared/DropdownList'
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
import {
Comparison,
ComparisonOperators,
LogicalOperator,
Table,
Variable,
} from 'models'
import React, { useEffect, useState } from 'react'
import { generate } from 'short-uuid'
import { useImmer } from 'use-immer'
type Props = {
initialComparisons: Table<Comparison>
logicalOperator: LogicalOperator
onLogicalOperatorChange: (logicalOperator: LogicalOperator) => void
onComparisonsChange: (comparisons: Table<Comparison>) => void
}
export const ComparisonsList = ({
initialComparisons,
logicalOperator,
onLogicalOperatorChange,
onComparisonsChange,
}: Props) => {
const [comparisons, setComparisons] = useImmer(initialComparisons)
const [showDeleteId, setShowDeleteId] = useState<string | undefined>()
useEffect(() => {
onComparisonsChange(comparisons)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [comparisons])
const createComparison = () => {
setComparisons((comparisons) => {
const id = generate()
comparisons.byId[id] = {
id,
comparisonOperator: ComparisonOperators.EQUAL,
}
comparisons.allIds.push(id)
})
}
const updateComparison = (
comparisonId: string,
updates: Partial<Omit<Comparison, 'id'>>
) =>
setComparisons((comparisons) => {
comparisons.byId[comparisonId] = {
...comparisons.byId[comparisonId],
...updates,
}
})
const deleteComparison = (comparisonId: string) => () => {
setComparisons((comparisons) => {
delete comparisons.byId[comparisonId]
const index = comparisons.allIds.indexOf(comparisonId)
if (index !== -1) comparisons.allIds.splice(index, 1)
})
}
const handleVariableSelected =
(comparisonId: string) => (variable: Variable) => {
updateComparison(comparisonId, { variableId: variable.id })
}
const handleComparisonOperatorSelected =
(comparisonId: string) => (dropdownItem: ComparisonOperators) =>
updateComparison(comparisonId, {
comparisonOperator: dropdownItem,
})
const handleLogicalOperatorSelected = (dropdownItem: LogicalOperator) =>
onLogicalOperatorChange(dropdownItem)
const handleValueChange = (comparisonId: string) => (value: string) =>
updateComparison(comparisonId, { value })
const handleMouseEnter = (comparisonId: string) => () => {
setShowDeleteId(comparisonId)
}
const handleMouseLeave = () => setShowDeleteId(undefined)
return (
<Stack spacing="4" py="4">
{comparisons.allIds.map((comparisonId, idx) => (
<>
{idx > 0 && (
<Flex justify="center">
<DropdownList<LogicalOperator>
currentItem={logicalOperator}
onItemSelect={handleLogicalOperatorSelected}
items={Object.values(LogicalOperator)}
/>
</Flex>
)}
<Flex
pos="relative"
onMouseEnter={handleMouseEnter(comparisonId)}
onMouseLeave={handleMouseLeave}
>
<Stack
key={comparisonId}
bgColor="blue.50"
p="4"
rounded="md"
flex="1"
>
<VariableSearchInput
initialVariableId={comparisons.byId[comparisonId].variableId}
onSelectVariable={handleVariableSelected(comparisonId)}
bgColor="white"
placeholder="Search for a variable"
/>
<DropdownList<ComparisonOperators>
currentItem={comparisons.byId[comparisonId].comparisonOperator}
onItemSelect={handleComparisonOperatorSelected(comparisonId)}
items={Object.values(ComparisonOperators)}
bgColor="white"
/>
{comparisons.byId[comparisonId].comparisonOperator !==
ComparisonOperators.IS_SET && (
<DebouncedInput
delay={100}
initialValue={comparisons.byId[comparisonId].value ?? ''}
onChange={handleValueChange(comparisonId)}
bgColor="white"
placeholder="Type a value..."
/>
)}
</Stack>
<Fade in={showDeleteId === comparisonId}>
<IconButton
icon={<TrashIcon />}
aria-label="Remove comparison"
onClick={deleteComparison(comparisonId)}
pos="absolute"
left="-10px"
top="-10px"
size="sm"
/>
</Fade>
</Flex>
</>
))}
<Button leftIcon={<PlusIcon />} onClick={createComparison} flexShrink={0}>
Add
</Button>
</Stack>
)
}

View File

@ -0,0 +1,27 @@
import { Comparison, ConditionOptions, LogicalOperator, Table } from 'models'
import React from 'react'
import { ComparisonsList } from './ComparisonsList'
type ConditionSettingsBodyProps = {
options: ConditionOptions
onOptionsChange: (options: ConditionOptions) => void
}
export const ConditionSettingsBody = ({
options,
onOptionsChange,
}: ConditionSettingsBodyProps) => {
const handleComparisonsChange = (comparisons: Table<Comparison>) =>
onOptionsChange({ ...options, comparisons })
const handleLogicalOperatorChange = (logicalOperator: LogicalOperator) =>
onOptionsChange({ ...options, logicalOperator })
return (
<ComparisonsList
initialComparisons={options.comparisons}
logicalOperator={options.logicalOperator ?? LogicalOperator.AND}
onLogicalOperatorChange={handleLogicalOperatorChange}
onComparisonsChange={handleComparisonsChange}
/>
)
}

View File

@ -0,0 +1 @@
export { ConditionSettingsBody } from './ConditonSettingsBody'