2
0

fix(results): 🐛 Collect prefilled variables in db

This commit is contained in:
Baptiste Arnaud
2022-02-17 16:08:01 +01:00
parent 0336bc2a42
commit aaf78e8a54
19 changed files with 454 additions and 507 deletions

View File

@@ -12,7 +12,7 @@ export const SetVariableContent = ({ step }: { step: SetVariableStep }) => {
<Text color={'gray.500'}> <Text color={'gray.500'}>
{variableName === '' && expression === '' {variableName === '' && expression === ''
? 'Click to edit...' ? 'Click to edit...'
: `${variableName} = ${expression}`} : `${variableName} ${expression ? `= ${expression}` : ``}`}
</Text> </Text>
) )
} }

View File

@@ -6,7 +6,6 @@ import { useRouter } from 'next/router'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { useStats } from 'services/analytics' import { useStats } from 'services/analytics'
import { isFreePlan } from 'services/user' import { isFreePlan } from 'services/user'
import { isDefined } from 'utils'
import { AnalyticsContent } from './AnalyticsContent' import { AnalyticsContent } from './AnalyticsContent'
import { SubmissionsContent } from './SubmissionContent' import { SubmissionsContent } from './SubmissionContent'

View File

@@ -66,7 +66,6 @@
"qs": "^6.10.3", "qs": "^6.10.3",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-frame-component": "^5.2.1",
"react-table": "^7.7.0", "react-table": "^7.7.0",
"short-uuid": "^4.2.0", "short-uuid": "^4.2.0",
"slate": "^0.72.8", "slate": "^0.72.8",

View File

@@ -38,6 +38,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
return res return res
.status(400) .status(400)
.send({ message: "User didn't accepted required scopes" }) .send({ message: "User didn't accepted required scopes" })
// console.log(tokens)
const { encryptedData, iv } = encrypt(tokens) const { encryptedData, iv } = encrypt(tokens)
const credentials = { const credentials = {
name: email, name: email,

View File

@@ -67,9 +67,9 @@ const createCredentials = () => {
expiry_date: 1642441058842, expiry_date: 1642441058842,
access_token: access_token:
'ya29.A0ARrdaM--PV_87ebjywDJpXKb77NBFJl16meVUapYdfNv6W6ZzqqC47fNaPaRjbDbOIIcp6f49cMaX5ndK9TAFnKwlVqz3nrK9nLKqgyDIhYsIq47smcAIZkK56SWPx3X3DwAFqRu2UPojpd2upWwo-3uJrod', 'ya29.A0ARrdaM--PV_87ebjywDJpXKb77NBFJl16meVUapYdfNv6W6ZzqqC47fNaPaRjbDbOIIcp6f49cMaX5ndK9TAFnKwlVqz3nrK9nLKqgyDIhYsIq47smcAIZkK56SWPx3X3DwAFqRu2UPojpd2upWwo-3uJrod',
// This token is linked to a mock Google account (typebot.test.user@gmail.com) // This token is linked to a test Google account (typebot.test.user@gmail.com)
refresh_token: refresh_token:
'1//03NRE9V8T-aayCgYIARAAGAMSNwF-L9Ir6zVzF-wm30psz0lbDJj5Y9OgqTO0cvBISODMW4QTR0VK40BLnOQgcHCHkb9c769TAhQ', '1//039xWRt8YaYa3CgYIARAAGAMSNwF-L9Iru9FyuTrDSa7lkSceggPho83kJt2J29G69iEhT1C6XV1vmo6bQS9puL_R2t8FIwR3gek',
}) })
return prisma.credentials.createMany({ return prisma.credentials.createMany({
data: [ data: [

View File

@@ -3,7 +3,7 @@ import shortId from 'short-uuid'
import { HStack, Text } from '@chakra-ui/react' import { HStack, Text } from '@chakra-ui/react'
import { CalendarIcon, CodeIcon } from 'assets/icons' import { CalendarIcon, CodeIcon } from 'assets/icons'
import { StepIcon } from 'components/editor/StepsSideBar/StepIcon' import { StepIcon } from 'components/editor/StepsSideBar/StepIcon'
import { isInputStep, sendRequest } from 'utils' import { byId, isInputStep, sendRequest } from 'utils'
import { isDefined } from '@udecode/plate-common' import { isDefined } from '@udecode/plate-common'
export const parseTypebotToPublicTypebot = ( export const parseTypebotToPublicTypebot = (
@@ -48,12 +48,14 @@ export const updatePublishedTypebot = async (
body: typebot, body: typebot,
}) })
export const parseSubmissionsColumns = ( type HeaderCell = {
typebot: PublicTypebot
): {
Header: JSX.Element Header: JSX.Element
accessor: string accessor: string
}[] => { }
export const parseSubmissionsColumns = (
typebot: PublicTypebot
): HeaderCell[] => {
const parsedBlocks = parseBlocksHeaders(typebot)
return [ return [
{ {
Header: ( Header: (
@@ -64,51 +66,58 @@ export const parseSubmissionsColumns = (
), ),
accessor: 'createdAt', accessor: 'createdAt',
}, },
...parseBlocksHeaders(typebot), ...parsedBlocks,
...parseVariablesHeaders(typebot), ...parseVariablesHeaders(typebot, parsedBlocks),
] ]
} }
const parseBlocksHeaders = (typebot: PublicTypebot) => const parseBlocksHeaders = (typebot: PublicTypebot) =>
typebot.blocks typebot.blocks
.filter((block) => typebot && block.steps.some((step) => isInputStep(step))) .filter((block) => typebot && block.steps.some((step) => isInputStep(step)))
.map((block) => { .reduce<HeaderCell[]>((headers, block) => {
const inputStep = block.steps.find((step) => isInputStep(step)) const inputStep = block.steps.find((step) => isInputStep(step))
if (!inputStep || !isInputStep(inputStep)) return if (
return { !inputStep ||
Header: ( !isInputStep(inputStep) ||
<HStack headers.find((h) => h.accessor === inputStep.options.variableId)
minW={
'isLong' in inputStep.options && inputStep.options.isLong
? '400px'
: '150px'
}
maxW="500px"
>
<StepIcon type={inputStep.type} />
<Text>{block.title}</Text>
</HStack>
),
accessor: block.id,
}
})
.filter(isDefined)
const parseVariablesHeaders = (typebot: PublicTypebot) =>
typebot.variables
.map((v) => {
const isVariableInInputStep = isDefined(
typebot.blocks.find((b) => {
const inputStep = b.steps.find((step) => isInputStep(step))
return (
inputStep &&
isInputStep(inputStep) &&
inputStep.options.variableId === v.id
)
})
) )
if (isVariableInInputStep) return return headers
return { const matchedVariableName =
inputStep.options.variableId &&
typebot.variables.find(byId(inputStep.options.variableId))?.name
return [
...headers,
{
Header: (
<HStack
minW={
'isLong' in inputStep.options && inputStep.options.isLong
? '400px'
: '150px'
}
maxW="500px"
>
<StepIcon type={inputStep.type} />
<Text>{matchedVariableName ?? block.title}</Text>
</HStack>
),
accessor: inputStep.options.variableId ?? block.id,
},
]
}, [])
const parseVariablesHeaders = (
typebot: PublicTypebot,
parsedBlocks: {
Header: JSX.Element
accessor: string
}[]
) =>
typebot.variables.reduce<HeaderCell[]>((headers, v) => {
if (parsedBlocks.find((b) => b.accessor === v.id)) return headers
return [
...headers,
{
Header: ( Header: (
<HStack minW={'150px'} maxW="500px"> <HStack minW={'150px'} maxW="500px">
<CodeIcon /> <CodeIcon />
@@ -116,6 +125,6 @@ const parseVariablesHeaders = (typebot: PublicTypebot) =>
</HStack> </HStack>
), ),
accessor: v.id, accessor: v.id,
} },
}) ]
.filter(isDefined) }, [])

View File

@@ -1,9 +1,9 @@
import { Result } from 'models' import { Result, VariableWithValue } from 'models'
import useSWRInfinite from 'swr/infinite' import useSWRInfinite from 'swr/infinite'
import { fetcher } from './utils' import { fetcher } from './utils'
import { stringify } from 'qs' import { stringify } from 'qs'
import { Answer } from 'db' import { Answer } from 'db'
import { sendRequest } from 'utils' import { isDefined, sendRequest } from 'utils'
const paginationLimit = 50 const paginationLimit = 50
@@ -98,8 +98,18 @@ export const parseDateToReadable = (dateStr: string): string => {
export const convertResultsToTableData = (results?: ResultWithAnswers[]) => export const convertResultsToTableData = (results?: ResultWithAnswers[]) =>
(results ?? []).map((result) => ({ (results ?? []).map((result) => ({
createdAt: parseDateToReadable(result.createdAt), createdAt: parseDateToReadable(result.createdAt),
...result.answers.reduce( ...[...result.answers, ...result.prefilledVariables].reduce<{
(o, answer) => ({ ...o, [answer.blockId]: answer.content }), [key: string]: string
{} }>((o, answerOrVariable) => {
), if ('blockId' in answerOrVariable) {
const answer = answerOrVariable as Answer
return {
...o,
[answer.variableId ?? answer.blockId]: answer.content,
}
}
const variable = answerOrVariable as VariableWithValue
if (isDefined(o[variable.id])) return o
return { ...o, [variable.id]: variable.value }
}, {}),
})) }))

View File

@@ -1,5 +1,5 @@
import { TypebotViewer } from 'bot-engine' import { TypebotViewer } from 'bot-engine'
import { Answer, PublicTypebot } from 'models' import { Answer, PublicTypebot, VariableWithValue } from 'models'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { upsertAnswer } from 'services/answer' import { upsertAnswer } from 'services/answer'
import { SEO } from '../components/Seo' import { SEO } from '../components/Seo'
@@ -19,21 +19,25 @@ export const TypebotPage = ({
isIE, isIE,
url, url,
}: TypebotPageProps & { typebot: PublicTypebot }) => { }: TypebotPageProps & { typebot: PublicTypebot }) => {
const [showTypebot, setShowTypebot] = useState(false)
const [error, setError] = useState<Error | undefined>( const [error, setError] = useState<Error | undefined>(
isIE ? new Error('Internet explorer is not supported') : undefined isIE ? new Error('Internet explorer is not supported') : undefined
) )
const [resultId, setResultId] = useState<string | undefined>() const [resultId, setResultId] = useState<string | undefined>()
// Workaround for react-frame-component bug (https://github.com/ryanseddon/react-frame-component/pull/207)
useEffect(() => { useEffect(() => {
initializeResult() setShowTypebot(true)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
const initializeResult = async () => { const initializeResult = async (variables: VariableWithValue[]) => {
const resultIdFromSession = sessionStorage.getItem(sessionStorageKey) const resultIdFromSession = sessionStorage.getItem(sessionStorageKey)
if (resultIdFromSession) setResultId(resultIdFromSession) if (resultIdFromSession) setResultId(resultIdFromSession)
else { else {
const { error, data: result } = await createResult(typebot.typebotId) const { error, data: result } = await createResult(
typebot.typebotId,
variables
)
if (error) setError(error) if (error) setError(error)
if (result) { if (result) {
setResultId(result.id) setResultId(result.id)
@@ -60,11 +64,12 @@ export const TypebotPage = ({
return ( return (
<div style={{ height: '100vh' }}> <div style={{ height: '100vh' }}>
<SEO url={url} chatbotName={typebot.name} /> <SEO url={url} chatbotName={typebot.name} />
{resultId && ( {showTypebot && (
<TypebotViewer <TypebotViewer
typebot={typebot} typebot={typebot}
onNewAnswer={handleNewAnswer} onNewAnswer={handleNewAnswer}
onCompleted={handleCompleted} onCompleted={handleCompleted}
onVariablesPrefilled={initializeResult}
/> />
)} )}
</div> </div>

View File

@@ -1,13 +1,17 @@
import { withSentry } from '@sentry/nextjs' import { withSentry } from '@sentry/nextjs'
import prisma from 'libs/prisma' import prisma from 'libs/prisma'
import { VariableWithValue } from 'models'
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from 'next'
import { methodNotAllowed } from 'utils' import { methodNotAllowed } from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'POST') { if (req.method === 'POST') {
const { typebotId } = JSON.parse(req.body) as { typebotId: string } const resultData = JSON.parse(req.body) as {
typebotId: string
prefilledVariables: VariableWithValue[]
}
const result = await prisma.result.create({ const result = await prisma.result.create({
data: { typebotId, isCompleted: false }, data: { ...resultData, isCompleted: false },
}) })
return res.send(result) return res.send(result)
} }

View File

@@ -1,12 +1,11 @@
import { withSentry } from '@sentry/nextjs' import { withSentry } from '@sentry/nextjs'
import prisma from 'libs/prisma' import prisma from 'libs/prisma'
import { Result } from 'models'
import { NextApiRequest, NextApiResponse } from 'next' import { NextApiRequest, NextApiResponse } from 'next'
import { methodNotAllowed } from 'utils' import { methodNotAllowed } from 'utils'
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'PATCH') { if (req.method === 'PATCH') {
const data = JSON.parse(req.body) as Result const data = JSON.parse(req.body) as { isCompleted: true }
const id = req.query.id.toString() const id = req.query.id.toString()
const result = await prisma.result.update({ const result = await prisma.result.update({
where: { id }, where: { id },

View File

@@ -1,11 +1,15 @@
import { Result } from 'db' import { Result } from 'db'
import { VariableWithValue } from 'models'
import { sendRequest } from 'utils' import { sendRequest } from 'utils'
export const createResult = async (typebotId: string) => { export const createResult = async (
typebotId: string,
prefilledVariables: VariableWithValue[]
) => {
return sendRequest<Result>({ return sendRequest<Result>({
url: `/api/results`, url: `/api/results`,
method: 'POST', method: 'POST',
body: { typebotId }, body: { typebotId, prefilledVariables },
}) })
} }

View File

@@ -21,8 +21,18 @@ export const ChatStep = ({
}) => { }) => {
const { addAnswer } = useAnswers() const { addAnswer } = useAnswers()
const handleInputSubmit = (content: string, isRetry: boolean) => { const handleInputSubmit = (
if (!isRetry) addAnswer({ stepId: step.id, blockId: step.blockId, content }) content: string,
isRetry: boolean,
variableId?: string
) => {
if (!isRetry)
addAnswer({
stepId: step.id,
blockId: step.blockId,
content,
variableId: variableId ?? null,
})
onTransitionEnd(content, isRetry) onTransitionEnd(content, isRetry)
} }
@@ -38,7 +48,7 @@ const InputChatStep = ({
onSubmit, onSubmit,
}: { }: {
step: InputStep step: InputStep
onSubmit: (value: string, isRetry: boolean) => void onSubmit: (value: string, isRetry: boolean, variableId?: string) => void
}) => { }) => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
const { addNewAvatarOffset } = useHostAvatars() const { addNewAvatarOffset } = useHostAvatars()
@@ -54,7 +64,7 @@ const InputChatStep = ({
const handleSubmit = (value: string) => { const handleSubmit = (value: string) => {
setAnswer(value) setAnswer(value)
onSubmit(value, !isInputValid(value, step.type)) onSubmit(value, !isInputValid(value, step.type), step.options.variableId)
} }
if (answer) { if (answer) {

View File

@@ -5,8 +5,8 @@ import { useFrame } from 'react-frame-component'
import { setCssVariablesValue } from '../services/theme' import { setCssVariablesValue } from '../services/theme'
import { useAnswers } from '../contexts/AnswersContext' import { useAnswers } from '../contexts/AnswersContext'
import { deepEqual } from 'fast-equals' import { deepEqual } from 'fast-equals'
import { Answer, Edge, PublicBlock, Theme } from 'models' import { Answer, Edge, PublicBlock, Theme, VariableWithValue } from 'models'
import { byId } from 'utils' import { byId, isNotDefined } from 'utils'
import { animateScroll as scroll } from 'react-scroll' import { animateScroll as scroll } from 'react-scroll'
import { useTypebot } from 'contexts/TypebotContext' import { useTypebot } from 'contexts/TypebotContext'
@@ -15,12 +15,14 @@ type Props = {
onNewBlockVisible: (edge: Edge) => void onNewBlockVisible: (edge: Edge) => void
onNewAnswer: (answer: Answer) => void onNewAnswer: (answer: Answer) => void
onCompleted: () => void onCompleted: () => void
onVariablesPrefilled?: (prefilledVariables: VariableWithValue[]) => void
} }
export const ConversationContainer = ({ export const ConversationContainer = ({
theme, theme,
onNewBlockVisible, onNewBlockVisible,
onNewAnswer, onNewAnswer,
onCompleted, onCompleted,
onVariablesPrefilled,
}: Props) => { }: Props) => {
const { typebot, updateVariableValue } = useTypebot() const { typebot, updateVariableValue } = useTypebot()
const { document: frameDocument } = useFrame() const { document: frameDocument } = useFrame()
@@ -48,19 +50,24 @@ export const ConversationContainer = ({
} }
useEffect(() => { useEffect(() => {
injectUrlParamsIntoVariables() const prefilledVariables = injectUrlParamsIntoVariables()
if (onVariablesPrefilled) onVariablesPrefilled(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
}, []) }, [])
const injectUrlParamsIntoVariables = () => { const injectUrlParamsIntoVariables = () => {
const urlParams = new URLSearchParams(location.search) const urlParams = new URLSearchParams(location.search)
const prefilledVariables: VariableWithValue[] = []
urlParams.forEach((value, key) => { urlParams.forEach((value, key) => {
const matchingVariable = typebot.variables.find( const matchingVariable = typebot.variables.find(
(v) => v.name.toLowerCase() === key.toLowerCase() (v) => v.name.toLowerCase() === key.toLowerCase()
) )
if (matchingVariable) updateVariableValue(matchingVariable?.id, value) if (isNotDefined(matchingVariable)) return
updateVariableValue(matchingVariable?.id, value)
prefilledVariables.push({ ...matchingVariable, value })
}) })
return prefilledVariables
} }
useEffect(() => { useEffect(() => {

View File

@@ -10,7 +10,13 @@ import phoneNumberInputStyle from 'react-phone-number-input/style.css'
import phoneSyle from '../assets/phone.css' import phoneSyle from '../assets/phone.css'
import { ConversationContainer } from './ConversationContainer' import { ConversationContainer } from './ConversationContainer'
import { AnswersContext } from '../contexts/AnswersContext' import { AnswersContext } from '../contexts/AnswersContext'
import { Answer, BackgroundType, Edge, PublicTypebot } from 'models' import {
Answer,
BackgroundType,
Edge,
PublicTypebot,
VariableWithValue,
} from 'models'
export type TypebotViewerProps = { export type TypebotViewerProps = {
typebot: PublicTypebot typebot: PublicTypebot
@@ -19,6 +25,7 @@ export type TypebotViewerProps = {
onNewBlockVisible?: (edge: Edge) => void onNewBlockVisible?: (edge: Edge) => void
onNewAnswer?: (answer: Answer) => void onNewAnswer?: (answer: Answer) => void
onCompleted?: () => void onCompleted?: () => void
onVariablesPrefilled?: (prefilledVariables: VariableWithValue[]) => void
} }
export const TypebotViewer = ({ export const TypebotViewer = ({
typebot, typebot,
@@ -27,6 +34,7 @@ export const TypebotViewer = ({
onNewBlockVisible, onNewBlockVisible,
onNewAnswer, onNewAnswer,
onCompleted, onCompleted,
onVariablesPrefilled,
}: TypebotViewerProps) => { }: TypebotViewerProps) => {
const containerBgColor = useMemo( const containerBgColor = useMemo(
() => () =>
@@ -84,6 +92,7 @@ export const TypebotViewer = ({
onNewBlockVisible={handleNewBlockVisible} onNewBlockVisible={handleNewBlockVisible}
onNewAnswer={handleNewAnswer} onNewAnswer={handleNewAnswer}
onCompleted={handleCompleted} onCompleted={handleCompleted}
onVariablesPrefilled={onVariablesPrefilled}
/> />
</div> </div>
{typebot.settings.general.isBrandingEnabled && ( {typebot.settings.general.isBrandingEnabled && (

View File

@@ -125,21 +125,23 @@ model PublicTypebot {
} }
model Result { model Result {
id String @id @default(cuid()) id String @id @default(cuid())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
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[]
isCompleted Boolean prefilledVariables Json[]
isCompleted Boolean
} }
model Answer { model Answer {
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)
stepId String stepId String
blockId String blockId String
content String variableId String?
content String
@@unique([resultId, blockId, stepId]) @@unique([resultId, blockId, stepId])
} }

View File

@@ -1,3 +1,7 @@
import { Result as ResultFromPrisma } from 'db' import { Result as ResultFromPrisma } from 'db'
import { VariableWithValue } from '.'
export type Result = Omit<ResultFromPrisma, 'createdAt'> & { createdAt: string } export type Result = Omit<
ResultFromPrisma,
'createdAt' | 'prefilledVariables'
> & { createdAt: string; prefilledVariables: VariableWithValue[] }

View File

@@ -3,3 +3,7 @@ export type Variable = {
name: string name: string
value?: string value?: string
} }
export type VariableWithValue = Omit<Variable, 'value'> & {
value: string
}

715
yarn.lock

File diff suppressed because it is too large Load Diff