Skip to content

Commit

Permalink
feat(inputs): ✨ Add Condition step
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno committed Jan 15, 2022
1 parent 4ccb7bc commit 2814a35
Show file tree
Hide file tree
Showing 30 changed files with 1,178 additions and 243 deletions.
6 changes: 6 additions & 0 deletions apps/builder/assets/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,9 @@ export const CheckSquareIcon = (props: IconProps) => (
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
</Icon>
)

export const FilterIcon = (props: IconProps) => (
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
</Icon>
)
63 changes: 49 additions & 14 deletions apps/builder/components/analytics/graph/Edges/Edge.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,76 @@
import { useAnalyticsGraph } from 'contexts/AnalyticsGraphProvider'
import React, { useMemo } from 'react'
import { useGraph } from 'contexts/GraphContext'
import React, { useEffect, useMemo, useState } from 'react'
import {
getAnchorsPosition,
computeFlowChartConnectorPath,
computeEdgePath,
getEndpointTopOffset,
} from 'services/graph'

type Props = { stepId: string }

export const Edge = ({ stepId }: Props) => {
const { typebot } = useAnalyticsGraph()
const step = typebot?.steps.byId[stepId]
const { sourceEndpoints, targetEndpoints, graphPosition } = useGraph()
const [sourceTop, setSourceTop] = useState(
getEndpointTopOffset(graphPosition, sourceEndpoints, stepId)
)
const [targetTop, setTargetTop] = useState(
getEndpointTopOffset(graphPosition, sourceEndpoints, step?.target?.stepId)
)

useEffect(() => {
const newSourceTop = getEndpointTopOffset(
graphPosition,
sourceEndpoints,
stepId
)
const sensibilityThreshold = 10
const newSourceTopIsTooClose =
sourceTop < newSourceTop + sensibilityThreshold &&
sourceTop > newSourceTop - sensibilityThreshold
if (newSourceTopIsTooClose) return
setSourceTop(newSourceTop)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [graphPosition])

useEffect(() => {
const newTargetTop = getEndpointTopOffset(
graphPosition,
targetEndpoints,
step?.target?.stepId
)
const sensibilityThreshold = 10
const newSourceTopIsTooClose =
targetTop < newTargetTop + sensibilityThreshold &&
targetTop > newTargetTop - sensibilityThreshold
if (newSourceTopIsTooClose) return
setTargetTop(newTargetTop)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [graphPosition])

const { sourceBlock, targetBlock, targetStepIndex } = useMemo(() => {
const { sourceBlock, targetBlock } = useMemo(() => {
if (!typebot) return {}
const step = typebot.steps.byId[stepId]
if (!step.target) return {}
if (!step?.target) return {}
const targetBlock = typebot.blocks.byId[step.target.blockId]
const targetStepIndex = step.target.stepId
? targetBlock.stepIds.indexOf(step.target.stepId)
: undefined
const sourceBlock = typebot.blocks.byId[step.blockId]
return {
sourceBlock,
targetBlock,
targetStepIndex,
}
}, [stepId, typebot])
}, [step?.blockId, step?.target, typebot])

const path = useMemo(() => {
if (!sourceBlock || !targetBlock) return ``
const anchorsPosition = getAnchorsPosition({
sourceBlock,
targetBlock,
sourceStepIndex: sourceBlock.stepIds.indexOf(stepId),
sourceChoiceItemIndex: targetStepIndex,
sourceTop,
targetTop,
})
return computeFlowChartConnectorPath(anchorsPosition)
}, [sourceBlock, stepId, targetBlock, targetStepIndex])
return computeEdgePath(anchorsPosition)
}, [sourceBlock, sourceTop, targetBlock, targetTop])

return (
<path
Expand Down
4 changes: 4 additions & 0 deletions apps/builder/components/board/StepTypesList/StepIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
CheckSquareIcon,
EditIcon,
EmailIcon,
FilterIcon,
FlagIcon,
GlobeIcon,
NumberIcon,
Expand Down Expand Up @@ -45,6 +46,9 @@ export const StepIcon = ({ type, ...props }: StepIconProps) => {
case LogicStepType.SET_VARIABLE: {
return <EditIcon {...props} />
}
case LogicStepType.CONDITION: {
return <FilterIcon {...props} />
}
case 'start': {
return <FlagIcon {...props} />
}
Expand Down
3 changes: 3 additions & 0 deletions apps/builder/components/board/StepTypesList/StepTypeLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export const StepTypeLabel = ({ type }: Props) => {
case LogicStepType.SET_VARIABLE: {
return <Text>Set variable</Text>
}
case LogicStepType.CONDITION: {
return <Text>Condition</Text>
}
default: {
return <></>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
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,
SetVariableOptions,
Step,
TextInputOptions,
} from 'models'
import { useRef } from 'react'
import {
TextInputSettingsBody,
NumberInputSettingsBody,
Expand All @@ -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'

Expand All @@ -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>
Expand All @@ -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) {
Expand Down Expand Up @@ -108,6 +125,14 @@ const SettingsPopoverBodyContent = ({ step }: Props) => {
/>
)
}
case LogicStepType.CONDITION: {
return (
<ConditionSettingsBody
options={step.options}
onOptionsChange={handleOptionsChange}
/>
)
}
default: {
return <></>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
)
}
Original file line number Diff line number Diff line change
@@ -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}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ConditionSettingsBody } from './ConditonSettingsBody'
Loading

2 comments on commit 2814a35

@vercel
Copy link

@vercel vercel bot commented on 2814a35 Jan 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

viewer-v2 – ./apps/viewer

typebot-io.vercel.app
viewer-v2-typebot-io.vercel.app
viewer-v2-git-main-typebot-io.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 2814a35 Jan 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

builder-v2 – ./apps/builder

builder-v2-git-main-typebot-io.vercel.app
next.typebot.io
builder-v2-typebot-io.vercel.app

Please sign in to comment.