2
0

fix(engine): 🐛 Save variables from webhooks in results

This commit is contained in:
Baptiste Arnaud
2022-03-28 17:07:47 +02:00
parent cd6c5c04c5
commit 60dcd5c246
12 changed files with 98 additions and 56 deletions

View File

@ -131,7 +131,7 @@ export const convertResultsToTableData = (
): { [key: string]: string }[] =>
(results ?? []).map((result) => ({
'Submitted at': parseDateToReadable(result.createdAt),
...[...result.answers, ...result.prefilledVariables].reduce<{
...[...result.answers, ...result.variables].reduce<{
[key: string]: string
}>((o, answerOrVariable) => {
if ('blockId' in answerOrVariable) {

View File

@ -54,11 +54,9 @@ export const TypebotPage = ({
setShowTypebot(true)
}
const handleVariablesPrefilled = async (
prefilledVariables: VariableWithValue[]
) => {
const handleNewVariables = async (variables: VariableWithValue[]) => {
if (!resultId) return setError(new Error('Result was not created'))
const { error } = await updateResult(resultId, { prefilledVariables })
const { error } = await updateResult(resultId, { variables })
if (error) setError(error)
}
@ -98,7 +96,7 @@ export const TypebotPage = ({
predefinedVariables={predefinedVariables}
onNewAnswer={handleNewAnswer}
onCompleted={handleCompleted}
onVariablesPrefilled={handleVariablesPrefilled}
onVariablesUpdated={handleNewVariables}
onNewLog={handleNewLog}
/>
)}

View File

@ -49,7 +49,7 @@ export const ChatBlock = ({
injectLinkedTypebot,
linkedTypebots,
} = useTypebot()
const { resultValues } = useAnswers()
const { resultValues, updateVariables } = useAnswers()
const [processedSteps, setProcessedSteps] = useState<Step[]>([])
const [displayedChunks, setDisplayedChunks] = useState<ChatDisplayChunk[]>([])
@ -104,6 +104,7 @@ export const ChatBlock = ({
typebot,
linkedTypebots,
updateVariableValue,
updateVariables,
injectLinkedTypebot,
onNewLog,
createEdge,
@ -121,6 +122,7 @@ export const ChatBlock = ({
variables: typebot.variables,
isPreview,
updateVariableValue,
updateVariables,
resultValues,
blocks: typebot.blocks,
onNewLog,

View File

@ -14,21 +14,19 @@ type Props = {
predefinedVariables?: { [key: string]: string | undefined }
onNewBlockVisible: (edge: Edge) => void
onCompleted: () => void
onVariablesPrefilled?: (prefilledVariables: VariableWithValue[]) => void
}
export const ConversationContainer = ({
theme,
predefinedVariables,
onNewBlockVisible,
onCompleted,
onVariablesPrefilled,
}: Props) => {
const { typebot, updateVariableValue } = useTypebot()
const { document: frameDocument } = useFrame()
const [displayedBlocks, setDisplayedBlocks] = useState<
{ block: Block; startStepIndex: number }[]
>([])
const { setPrefilledVariables } = useAnswers()
const { updateVariables } = useAnswers()
const bottomAnchor = useRef<HTMLDivElement | null>(null)
const scrollableContainer = useRef<HTMLDivElement | null>(null)
@ -53,8 +51,7 @@ export const ConversationContainer = ({
useEffect(() => {
const prefilledVariables = injectPredefinedVariables(predefinedVariables)
if (onVariablesPrefilled) onVariablesPrefilled(prefilledVariables)
setPrefilledVariables(prefilledVariables)
updateVariables(prefilledVariables)
displayNextBlock(typebot.blocks[0].steps[0].outgoingEdgeId)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

View File

@ -31,7 +31,7 @@ export type TypebotViewerProps = {
onNewAnswer?: (answer: Answer) => void
onNewLog?: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
onCompleted?: () => void
onVariablesPrefilled?: (prefilledVariables: VariableWithValue[]) => void
onVariablesUpdated?: (variables: VariableWithValue[]) => void
}
export const TypebotViewer = ({
@ -44,7 +44,7 @@ export const TypebotViewer = ({
onNewBlockVisible,
onNewAnswer,
onCompleted,
onVariablesPrefilled,
onVariablesUpdated,
}: TypebotViewerProps) => {
const containerBgColor = useMemo(
() =>
@ -93,7 +93,10 @@ export const TypebotViewer = ({
isPreview={isPreview}
onNewLog={handleNewLog}
>
<AnswersContext onNewAnswer={handleNewAnswer}>
<AnswersContext
onNewAnswer={handleNewAnswer}
onVariablesUpdated={onVariablesUpdated}
>
<div
className="flex text-base overflow-hidden bg-cover h-screen w-screen flex-col items-center typebot-container"
style={{
@ -108,7 +111,6 @@ export const TypebotViewer = ({
onNewBlockVisible={handleNewBlockVisible}
onCompleted={handleCompleted}
predefinedVariables={predefinedVariables}
onVariablesPrefilled={onVariablesPrefilled}
/>
</div>
{typebot.settings.general.isBrandingEnabled && (

View File

@ -4,7 +4,7 @@ import React, { createContext, ReactNode, useContext, useState } from 'react'
const answersContext = createContext<{
resultValues: ResultValues
addAnswer: (answer: Answer) => void
setPrefilledVariables: (variables: VariableWithValue[]) => void
updateVariables: (variables: VariableWithValue[]) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})
@ -12,13 +12,15 @@ const answersContext = createContext<{
export const AnswersContext = ({
children,
onNewAnswer,
onVariablesUpdated,
}: {
onNewAnswer: (answer: Answer) => void
onVariablesUpdated?: (variables: VariableWithValue[]) => void
children: ReactNode
}) => {
const [resultValues, setResultValues] = useState<ResultValues>({
answers: [],
prefilledVariables: [],
variables: [],
createdAt: new Date().toISOString(),
})
@ -30,18 +32,22 @@ export const AnswersContext = ({
onNewAnswer(answer)
}
const setPrefilledVariables = (variables: VariableWithValue[]) =>
setResultValues((resultValues) => ({
const updateVariables = (variables: VariableWithValue[]) =>
setResultValues((resultValues) => {
const updatedVariables = [...resultValues.variables, ...variables]
if (onVariablesUpdated) onVariablesUpdated(updatedVariables)
return {
...resultValues,
prefilledVariables: variables,
}))
variables: updatedVariables,
}
})
return (
<answersContext.Provider
value={{
resultValues,
addAnswer,
setPrefilledVariables,
updateVariables,
}}
>
{children}

View File

@ -15,9 +15,10 @@ import {
ZapierStep,
ResultValues,
Block,
VariableWithValue,
} from 'models'
import { stringify } from 'qs'
import { sendRequest } from 'utils'
import { byId, sendRequest } from 'utils'
import { sendGaEvent } from '../../lib/gtag'
import { parseVariables, parseVariablesInObject } from './variable'
@ -30,6 +31,7 @@ type IntegrationContext = {
variables: Variable[]
resultValues: ResultValues
blocks: Block[]
updateVariables: (variables: VariableWithValue[]) => void
updateVariableValue: (variableId: string, value: string) => void
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
}
@ -141,7 +143,13 @@ const updateRowInGoogleSheets = async (
const getRowFromGoogleSheets = async (
options: GoogleSheetsGetOptions,
{ variables, updateVariableValue, apiHost, onNewLog }: IntegrationContext
{
variables,
updateVariableValue,
updateVariables,
apiHost,
onNewLog,
}: IntegrationContext
) => {
if (!options.referenceCell || !options.cellsToExtract) return
const queryParams = stringify(
@ -167,9 +175,23 @@ const getRowFromGoogleSheets = async (
)
)
if (!data) return
options.cellsToExtract.forEach((cell) =>
updateVariableValue(cell.variableId ?? '', data[cell.column ?? ''])
const newVariables = options.cellsToExtract.reduce<VariableWithValue[]>(
(newVariables, cell) => {
const existingVariable = variables.find(byId(cell.variableId))
const value = data[cell.column ?? '']
if (!existingVariable || !value) return newVariables
updateVariableValue(existingVariable.id, value)
return [
...newVariables,
{
...existingVariable,
value,
},
]
},
[]
)
updateVariables(newVariables)
}
const parseCellValues = (
cells: Cell[],
@ -191,6 +213,7 @@ const executeWebhook = async (
stepId,
variables,
updateVariableValue,
updateVariables,
typebotId,
apiHost,
resultValues,
@ -218,13 +241,19 @@ const executeWebhook = async (
: 'Webhook successfuly executed',
details: JSON.stringify(error ?? data, null, 2).substring(0, 1000),
})
step.options.responseVariableMapping.forEach((varMapping) => {
if (!varMapping?.bodyPath || !varMapping.variableId) return
const newVariables = step.options.responseVariableMapping.reduce<
VariableWithValue[]
>((newVariables, varMapping) => {
if (!varMapping?.bodyPath || !varMapping.variableId) return newVariables
const existingVariable = variables.find(byId(varMapping.variableId))
if (!existingVariable) return newVariables
const value = Function(
`return (${JSON.stringify(data)}).${varMapping?.bodyPath}`
)()
updateVariableValue(varMapping.variableId, value)
})
updateVariableValue(existingVariable?.id, value)
return [...newVariables, { ...existingVariable, value }]
}, [])
updateVariables(newVariables)
return step.outgoingEdgeId
}

View File

@ -15,6 +15,7 @@ import {
PublicTypebot,
Typebot,
Edge,
VariableWithValue,
} from 'models'
import { byId, isDefined, isNotDefined, sendRequest } from 'utils'
import { sanitizeUrl } from './utils'
@ -28,6 +29,7 @@ type LogicContext = {
typebot: PublicTypebot
linkedTypebots: LinkedTypebot[]
updateVariableValue: (variableId: string, value: string) => void
updateVariables: (variables: VariableWithValue[]) => void
injectLinkedTypebot: (typebot: Typebot | PublicTypebot) => LinkedTypebot
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
createEdge: (edge: Edge) => void
@ -56,7 +58,7 @@ export const executeLogic = async (
const executeSetVariable = (
step: SetVariableStep,
{ typebot: { variables }, updateVariableValue }: LogicContext
{ typebot: { variables }, updateVariableValue, updateVariables }: LogicContext
): EdgeId | undefined => {
if (!step.options?.variableId || !step.options.expressionToEvaluate)
return step.outgoingEdgeId
@ -64,7 +66,10 @@ const executeSetVariable = (
const evaluatedExpression = evaluateExpression(
parseVariables(variables)(expression)
)
updateVariableValue(step.options.variableId, evaluatedExpression)
const existingVariable = variables.find(byId(step.options.variableId))
if (!existingVariable) return step.outgoingEdgeId
updateVariableValue(existingVariable.id, evaluatedExpression)
updateVariables([{ ...existingVariable, value: evaluatedExpression }])
return step.outgoingEdgeId
}

View File

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Result"
RENAME COLUMN "prefilledVariables" "variables"

View File

@ -184,7 +184,7 @@ model Result {
typebotId String
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
answers Answer[]
prefilledVariables Json[]
variables Json[]
isCompleted Boolean
logs Log[]
}

View File

@ -1,16 +1,16 @@
import { Result as ResultFromPrisma } from 'db'
import { Answer, InputStepType, VariableWithValue } from '.'
export type Result = Omit<
ResultFromPrisma,
'createdAt' | 'prefilledVariables'
> & { createdAt: string; prefilledVariables: VariableWithValue[] }
export type Result = Omit<ResultFromPrisma, 'createdAt' | 'variables'> & {
createdAt: string
variables: VariableWithValue[]
}
export type ResultWithAnswers = Result & { answers: Answer[] }
export type ResultValues = Pick<
ResultWithAnswers,
'answers' | 'createdAt' | 'prefilledVariables'
'answers' | 'createdAt' | 'variables'
>
export type ResultHeaderCell = {

View File

@ -91,14 +91,14 @@ export const parseAnswers =
({
createdAt,
answers,
prefilledVariables,
}: Pick<ResultWithAnswers, 'createdAt' | 'answers' | 'prefilledVariables'>): {
variables: resultVariables,
}: Pick<ResultWithAnswers, 'createdAt' | 'answers' | 'variables'>): {
[key: string]: string
} => {
const header = parseResultHeader({ blocks, variables })
return {
submittedAt: createdAt,
...[...answers, ...prefilledVariables].reduce<{
...[...answers, ...resultVariables].reduce<{
[key: string]: string
}>((o, answerOrVariable) => {
if ('blockId' in answerOrVariable) {