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:
@ -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])
|
||||
}
|
||||
|
||||
|
@ -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} />
|
||||
}
|
||||
}
|
||||
|
@ -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 }),
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
/>
|
||||
))}
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user