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 }[] => ): { [key: string]: string }[] =>
(results ?? []).map((result) => ({ (results ?? []).map((result) => ({
'Submitted at': parseDateToReadable(result.createdAt), 'Submitted at': parseDateToReadable(result.createdAt),
...[...result.answers, ...result.prefilledVariables].reduce<{ ...[...result.answers, ...result.variables].reduce<{
[key: string]: string [key: string]: string
}>((o, answerOrVariable) => { }>((o, answerOrVariable) => {
if ('blockId' in answerOrVariable) { if ('blockId' in answerOrVariable) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -56,8 +56,8 @@ model User {
customDomains CustomDomain[] customDomains CustomDomain[]
apiToken String? apiToken String?
CollaboratorsOnTypebots CollaboratorsOnTypebots[] CollaboratorsOnTypebots CollaboratorsOnTypebots[]
company String? company String?
onboardingCategories String[] onboardingCategories String[]
} }
model CustomDomain { model CustomDomain {
@ -178,22 +178,22 @@ model PublicTypebot {
} }
model Result { model Result {
id String @id @default(cuid()) id String @id @default(cuid())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
typebotId String typebotId String
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade) typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
answers Answer[] answers Answer[]
prefilledVariables Json[] variables Json[]
isCompleted Boolean isCompleted Boolean
logs Log[] logs Log[]
} }
model Log { model Log {
id String @id @default(cuid()) id String @id @default(cuid())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
resultId String resultId String
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade) result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
status String status String
description String description String
details String? details String?

View File

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

View File

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