2
0

chore(editor): ♻️ Revert tables to arrays

Yet another refacto. I improved many many mechanisms on this one including dnd. It is now end 2 end tested 🎉
This commit is contained in:
Baptiste Arnaud
2022-02-04 19:00:08 +01:00
parent 8a350eee6c
commit 524ef0812c
123 changed files with 2998 additions and 3112 deletions

View File

@ -4,7 +4,7 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group'
import { ChatStep } from './ChatStep'
import { AvatarSideContainer } from './AvatarSideContainer'
import { HostAvatarsContext } from '../../contexts/HostAvatarsContext'
import { Step } from 'models'
import { PublicStep } from 'models'
import { useTypebot } from '../../contexts/TypebotContext'
import {
isBubbleStep,
@ -14,26 +14,28 @@ import {
isLogicStep,
} from 'utils'
import { executeLogic } from 'services/logic'
import { getSingleChoiceTargetId } from 'services/inputs'
import { executeIntegration } from 'services/integration'
type ChatBlockProps = {
stepIds: string[]
startStepId?: string
steps: PublicStep[]
startStepIndex: number
blockIndex: number
onBlockEnd: (edgeId?: string) => void
}
export const ChatBlock = ({
stepIds,
startStepId,
steps,
startStepIndex,
blockIndex,
onBlockEnd,
}: ChatBlockProps) => {
const { typebot, updateVariableValue } = useTypebot()
const [displayedSteps, setDisplayedSteps] = useState<Step[]>([])
const [displayedSteps, setDisplayedSteps] = useState<PublicStep[]>([])
const currentStepIndex = displayedSteps.length - 1
useEffect(() => {
const nextStep =
typebot.steps.byId[startStepId ?? stepIds[displayedSteps.length]]
const nextStep = steps[startStepIndex]
if (nextStep) setDisplayedSteps([...displayedSteps, nextStep])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@ -60,6 +62,7 @@ export const ChatBlock = ({
typebot.typebotId,
currentStep,
typebot.variables,
{ blockIndex, stepIndex: currentStepIndex },
updateVariableValue
)
nextEdgeId ? onBlockEnd(nextEdgeId) : displayNextStep()
@ -85,18 +88,17 @@ export const ChatBlock = ({
}
const isSingleChoiceStep =
isChoiceInput(currentStep) && !currentStep.options.isMultipleChoice
if (isSingleChoiceStep)
return onBlockEnd(
getSingleChoiceTargetId(
currentStep,
typebot.choiceItems,
answerContent
)
if (isSingleChoiceStep) {
onBlockEnd(
currentStep.items.find((i) => i.content === answerContent)
?.outgoingEdgeId
)
if (currentStep?.edgeId || displayedSteps.length === stepIds.length)
return onBlockEnd(currentStep.edgeId)
}
if (currentStep?.outgoingEdgeId || displayedSteps.length === steps.length)
return onBlockEnd(currentStep.outgoingEdgeId)
}
const nextStep = typebot.steps.byId[stepIds[displayedSteps.length]]
const nextStep = steps[displayedSteps.length]
if (nextStep) setDisplayedSteps([...displayedSteps, nextStep])
}

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'
import { useAnswers } from '../../../contexts/AnswersContext'
import { useHostAvatars } from '../../../contexts/HostAvatarsContext'
import { InputStep, InputStepType, Step } from 'models'
import { InputStep, InputStepType, PublicStep, Step } from 'models'
import { GuestBubble } from './bubbles/GuestBubble'
import { TextForm } from './inputs/TextForm'
import { isBubbleStep, isInputStep } from 'utils'
@ -13,7 +13,7 @@ export const ChatStep = ({
step,
onTransitionEnd,
}: {
step: Step
step: PublicStep
onTransitionEnd: (answerContent?: string) => void
}) => {
const { addAnswer } = useAnswers()
@ -63,6 +63,6 @@ const InputChatStep = ({
case InputStepType.DATE:
return <DateForm options={step.options} onSubmit={handleSubmit} />
case InputStepType.CHOICE:
return <ChoiceForm options={step.options} onSubmit={handleSubmit} />
return <ChoiceForm step={step} onSubmit={handleSubmit} />
}
}

View File

@ -2,7 +2,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'
import { useHostAvatars } from 'contexts/HostAvatarsContext'
import { useTypebot } from 'contexts/TypebotContext'
import {
Table,
Variable,
VideoBubbleContent,
VideoBubbleContentType,
@ -83,7 +82,7 @@ const VideoContent = ({
}: {
content?: VideoBubbleContent
isTyping: boolean
variables: Table<Variable>
variables: Variable[]
}) => {
const url = useMemo(
() => parseVariables({ text: content?.url, variables: variables }),

View File

@ -1,65 +1,61 @@
import { ChoiceInputOptions } from 'models'
import React, { useMemo, useState } from 'react'
import { filterTable } from 'utils'
import { useTypebot } from '../../../../contexts/TypebotContext'
import { ChoiceInputStep } from 'models'
import React, { useState } from 'react'
import { SendButton } from './SendButton'
type ChoiceFormProps = {
options?: ChoiceInputOptions
step: ChoiceInputStep
onSubmit: (value: string) => void
}
export const ChoiceForm = ({ options, onSubmit }: ChoiceFormProps) => {
const { typebot } = useTypebot()
const items = useMemo(
() => filterTable(options?.itemIds ?? [], typebot.choiceItems),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
)
const [selectedIds, setSelectedIds] = useState<string[]>([])
export const ChoiceForm = ({ step, onSubmit }: ChoiceFormProps) => {
const [selectedIndices, setSelectedIndices] = useState<number[]>([])
const handleClick = (itemId: string) => (e: React.MouseEvent) => {
const handleClick = (itemIndex: number) => (e: React.MouseEvent) => {
e.preventDefault()
if (options?.isMultipleChoice) toggleSelectedItemId(itemId)
else onSubmit(items.byId[itemId].content ?? '')
if (step.options?.isMultipleChoice) toggleSelectedItemIndex(itemIndex)
else onSubmit(step.items[itemIndex].content ?? '')
}
const toggleSelectedItemId = (itemId: string) => {
const existingIndex = selectedIds.indexOf(itemId)
const toggleSelectedItemIndex = (itemIndex: number) => {
const existingIndex = selectedIndices.indexOf(itemIndex)
if (existingIndex !== -1) {
selectedIds.splice(existingIndex, 1)
setSelectedIds([...selectedIds])
selectedIndices.splice(existingIndex, 1)
setSelectedIndices([...selectedIndices])
} else {
setSelectedIds([...selectedIds, itemId])
setSelectedIndices([...selectedIndices, itemIndex])
}
}
const handleSubmit = () =>
onSubmit(selectedIds.map((itemId) => items.byId[itemId].content).join(', '))
onSubmit(
selectedIndices
.map((itemIndex) => step.items[itemIndex].content)
.join(', ')
)
return (
<form className="flex flex-col" onSubmit={handleSubmit}>
<div className="flex flex-wrap">
{options?.itemIds.map((itemId) => (
{step.items.map((item, idx) => (
<button
key={itemId}
role={options?.isMultipleChoice ? 'checkbox' : 'button'}
onClick={handleClick(itemId)}
key={item.id}
role={step.options?.isMultipleChoice ? 'checkbox' : 'button'}
onClick={handleClick(idx)}
className={
'py-2 px-4 font-semibold rounded-md transition-all filter hover:brightness-90 active:brightness-75 duration-100 focus:outline-none mr-2 mb-2 typebot-button ' +
(selectedIds.includes(itemId) || !options?.isMultipleChoice
(selectedIndices.includes(idx) || !step.options?.isMultipleChoice
? 'active'
: '')
}
data-testid="button"
>
{items.byId[itemId].content}
{item.content}
</button>
))}
</div>
<div className="flex">
{selectedIds.length > 0 && (
<SendButton label={options?.buttonLabel ?? 'Send'} />
{selectedIndices.length > 0 && (
<SendButton label={step.options?.buttonLabel ?? 'Send'} />
)}
</div>
</form>

View File

@ -5,11 +5,12 @@ import { useFrame } from 'react-frame-component'
import { setCssVariablesValue } from '../services/theme'
import { useAnswers } from '../contexts/AnswersContext'
import { deepEqual } from 'fast-equals'
import { Answer, Block, PublicTypebot } from 'models'
import { Answer, Edge, PublicBlock, PublicTypebot } from 'models'
import { byId } from 'utils'
type Props = {
typebot: PublicTypebot
onNewBlockVisible: (edgeId: string) => void
onNewBlockVisible: (edge: Edge) => void
onNewAnswer: (answer: Answer) => void
onCompleted: () => void
}
@ -21,30 +22,29 @@ export const ConversationContainer = ({
}: Props) => {
const { document: frameDocument } = useFrame()
const [displayedBlocks, setDisplayedBlocks] = useState<
{ block: Block; startStepId?: string }[]
{ block: PublicBlock; startStepIndex: number }[]
>([])
const [localAnswer, setLocalAnswer] = useState<Answer | undefined>()
const { answers } = useAnswers()
const bottomAnchor = useRef<HTMLDivElement | null>(null)
const displayNextBlock = (edgeId?: string) => {
const edge = typebot.edges.byId[edgeId ?? '']
if (!edge) return onCompleted()
const nextBlock = {
block: typebot.blocks.byId[edge.to.blockId],
startStepId: edge.to.stepId,
}
const nextEdge = typebot.edges.find(byId(edgeId))
if (!nextEdge) return onCompleted()
const nextBlock = typebot.blocks.find(byId(nextEdge.to.blockId))
if (!nextBlock) return onCompleted()
onNewBlockVisible(edge.id)
setDisplayedBlocks([...displayedBlocks, nextBlock])
const startStepIndex = nextEdge.to.stepId
? nextBlock.steps.findIndex(byId(nextEdge.to.stepId))
: 0
onNewBlockVisible(nextEdge)
setDisplayedBlocks([
...displayedBlocks,
{ block: nextBlock, startStepIndex },
])
}
useEffect(() => {
const blocks = typebot.blocks
const firstEdgeId =
typebot.steps.byId[blocks.byId[blocks.allIds[0]].stepIds[0]].edgeId
if (!firstEdgeId) return
displayNextBlock(firstEdgeId)
displayNextBlock(typebot.blocks[0].steps[0].outgoingEdgeId)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@ -68,8 +68,9 @@ export const ConversationContainer = ({
{displayedBlocks.map((displayedBlock, idx) => (
<ChatBlock
key={displayedBlock.block.id + idx}
stepIds={displayedBlock.block.stepIds}
startStepId={displayedBlock.startStepId}
steps={displayedBlock.block.steps}
startStepIndex={displayedBlock.startStepIndex}
blockIndex={idx}
onBlockEnd={displayNextBlock}
/>
))}

View File

@ -10,11 +10,11 @@ import phoneNumberInputStyle from 'react-phone-number-input/style.css'
import phoneSyle from '../assets/phone.css'
import { ConversationContainer } from './ConversationContainer'
import { AnswersContext } from '../contexts/AnswersContext'
import { Answer, BackgroundType, PublicTypebot } from 'models'
import { Answer, BackgroundType, Edge, PublicTypebot } from 'models'
export type TypebotViewerProps = {
typebot: PublicTypebot
onNewBlockVisible?: (edgeId: string) => void
onNewBlockVisible?: (edge: Edge) => void
onNewAnswer?: (answer: Answer) => void
onCompleted?: () => void
}
@ -31,8 +31,8 @@ export const TypebotViewer = ({
: 'transparent',
[typebot?.theme?.general?.background]
)
const handleNewBlockVisible = (blockId: string) => {
if (onNewBlockVisible) onNewBlockVisible(blockId)
const handleNewBlockVisible = (edge: Edge) => {
if (onNewBlockVisible) onNewBlockVisible(edge)
}
const handleNewAnswer = (answer: Answer) => {
if (onNewAnswer) onNewAnswer(answer)