fix(results): 🐛 Display results for blocks w/ multiple inputs
This commit is contained in:
@ -2,14 +2,13 @@
|
|||||||
/* eslint-disable react/jsx-key */
|
/* eslint-disable react/jsx-key */
|
||||||
import { Button, chakra, Checkbox, Flex, HStack, Text } from '@chakra-ui/react'
|
import { Button, chakra, Checkbox, Flex, HStack, Text } from '@chakra-ui/react'
|
||||||
import { AlignLeftTextIcon } from 'assets/icons'
|
import { AlignLeftTextIcon } from 'assets/icons'
|
||||||
import { PublicTypebot } from 'models'
|
|
||||||
import React, { useEffect, useMemo, useRef } from 'react'
|
import React, { useEffect, useMemo, useRef } from 'react'
|
||||||
import { Hooks, useRowSelect, useTable } from 'react-table'
|
import { Hooks, useRowSelect, useTable } from 'react-table'
|
||||||
import { parseSubmissionsColumns } from 'services/publicTypebot'
|
import { parseSubmissionsColumns, ResultHeaderCell } from 'services/typebots'
|
||||||
import { LoadingRows } from './LoadingRows'
|
import { LoadingRows } from './LoadingRows'
|
||||||
|
|
||||||
type SubmissionsTableProps = {
|
type SubmissionsTableProps = {
|
||||||
blocksAndVariables: Pick<PublicTypebot, 'blocks' | 'variables'>
|
resultHeader: ResultHeaderCell[]
|
||||||
data?: any
|
data?: any
|
||||||
hasMore?: boolean
|
hasMore?: boolean
|
||||||
onNewSelection: (indices: number[]) => void
|
onNewSelection: (indices: number[]) => void
|
||||||
@ -18,7 +17,7 @@ type SubmissionsTableProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SubmissionsTable = ({
|
export const SubmissionsTable = ({
|
||||||
blocksAndVariables,
|
resultHeader,
|
||||||
data,
|
data,
|
||||||
hasMore,
|
hasMore,
|
||||||
onNewSelection,
|
onNewSelection,
|
||||||
@ -26,8 +25,8 @@ export const SubmissionsTable = ({
|
|||||||
onLogOpenIndex,
|
onLogOpenIndex,
|
||||||
}: SubmissionsTableProps) => {
|
}: SubmissionsTableProps) => {
|
||||||
const columns: any = useMemo(
|
const columns: any = useMemo(
|
||||||
() => parseSubmissionsColumns(blocksAndVariables),
|
() => parseSubmissionsColumns(resultHeader),
|
||||||
[blocksAndVariables]
|
[resultHeader]
|
||||||
)
|
)
|
||||||
const bottomElement = useRef<HTMLDivElement | null>(null)
|
const bottomElement = useRef<HTMLDivElement | null>(null)
|
||||||
const tableWrapper = useRef<HTMLDivElement | null>(null)
|
const tableWrapper = useRef<HTMLDivElement | null>(null)
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
deleteAllResults,
|
deleteAllResults,
|
||||||
deleteResults,
|
deleteResults,
|
||||||
getAllResults,
|
getAllResults,
|
||||||
|
parseResultHeader,
|
||||||
useResults,
|
useResults,
|
||||||
} from 'services/typebots'
|
} from 'services/typebots'
|
||||||
import { unparse } from 'papaparse'
|
import { unparse } from 'papaparse'
|
||||||
@ -49,6 +50,8 @@ export const SubmissionsContent = ({
|
|||||||
].filter(isDefined),
|
].filter(isDefined),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resultHeader = parseResultHeader(blocksAndVariables)
|
||||||
|
|
||||||
const { data, mutate, setSize, hasMore } = useResults({
|
const { data, mutate, setSize, hasMore } = useResults({
|
||||||
typebotId,
|
typebotId,
|
||||||
onError: (err) => toast({ title: err.name, description: err.message }),
|
onError: (err) => toast({ title: err.name, description: err.message }),
|
||||||
@ -117,14 +120,12 @@ export const SubmissionsContent = ({
|
|||||||
if (!publishedTypebot) return []
|
if (!publishedTypebot) return []
|
||||||
const { data, error } = await getAllResults(typebotId)
|
const { data, error } = await getAllResults(typebotId)
|
||||||
if (error) toast({ description: error.message, title: error.name })
|
if (error) toast({ description: error.message, title: error.name })
|
||||||
return convertResultsToTableData(blocksAndVariables)(data?.results)
|
return convertResultsToTableData(data?.results, resultHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tableData: { [key: string]: string }[] = useMemo(
|
const tableData: { [key: string]: string }[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
publishedTypebot
|
publishedTypebot ? convertResultsToTableData(results, resultHeader) : [],
|
||||||
? convertResultsToTableData(blocksAndVariables)(results)
|
|
||||||
: [],
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[results]
|
[results]
|
||||||
)
|
)
|
||||||
@ -159,7 +160,7 @@ export const SubmissionsContent = ({
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<SubmissionsTable
|
<SubmissionsTable
|
||||||
blocksAndVariables={blocksAndVariables}
|
resultHeader={resultHeader}
|
||||||
data={tableData}
|
data={tableData}
|
||||||
onNewSelection={handleNewSelection}
|
onNewSelection={handleNewSelection}
|
||||||
onScrollToBottom={handleScrolledToBottom}
|
onScrollToBottom={handleScrolledToBottom}
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import { Block, PublicTypebot, Typebot, Variable } from 'models'
|
import { PublicTypebot, Typebot } from 'models'
|
||||||
import shortId from 'short-uuid'
|
import shortId from 'short-uuid'
|
||||||
import { HStack, Text } from '@chakra-ui/react'
|
import { sendRequest } from 'utils'
|
||||||
import { CalendarIcon, CodeIcon } from 'assets/icons'
|
|
||||||
import { StepIcon } from 'components/editor/StepsSideBar/StepIcon'
|
|
||||||
import { byId, isInputStep, sendRequest } from 'utils'
|
|
||||||
|
|
||||||
export const parseTypebotToPublicTypebot = (
|
export const parseTypebotToPublicTypebot = (
|
||||||
typebot: Typebot
|
typebot: Typebot
|
||||||
@ -58,96 +55,3 @@ export const updatePublishedTypebot = async (
|
|||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: typebot,
|
body: typebot,
|
||||||
})
|
})
|
||||||
|
|
||||||
type HeaderCell = {
|
|
||||||
Header: JSX.Element
|
|
||||||
accessor: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const parseSubmissionsColumns = (blocksAndVariables: {
|
|
||||||
blocks: Block[]
|
|
||||||
variables: Variable[]
|
|
||||||
}): HeaderCell[] => {
|
|
||||||
const parsedBlocks = parseBlocksHeaders(blocksAndVariables)
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
Header: (
|
|
||||||
<HStack>
|
|
||||||
<CalendarIcon />
|
|
||||||
<Text>Submitted at</Text>
|
|
||||||
</HStack>
|
|
||||||
),
|
|
||||||
accessor: 'Submitted at',
|
|
||||||
},
|
|
||||||
...parsedBlocks,
|
|
||||||
...parseVariablesHeaders(blocksAndVariables.variables, parsedBlocks),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseBlocksHeaders = ({
|
|
||||||
blocks,
|
|
||||||
variables,
|
|
||||||
}: {
|
|
||||||
blocks: Block[]
|
|
||||||
variables: Variable[]
|
|
||||||
}) =>
|
|
||||||
blocks
|
|
||||||
.filter((block) => block.steps.some((step) => isInputStep(step)))
|
|
||||||
.reduce<HeaderCell[]>((headers, block) => {
|
|
||||||
const inputStep = block.steps.find((step) => isInputStep(step))
|
|
||||||
if (
|
|
||||||
!inputStep ||
|
|
||||||
!isInputStep(inputStep) ||
|
|
||||||
headers.find(
|
|
||||||
(h) =>
|
|
||||||
h.accessor ===
|
|
||||||
variables.find(byId(inputStep.options.variableId))?.name
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return headers
|
|
||||||
const matchedVariableName =
|
|
||||||
inputStep.options.variableId &&
|
|
||||||
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: matchedVariableName ?? block.title,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const parseVariablesHeaders = (
|
|
||||||
variables: Variable[],
|
|
||||||
parsedBlocks: {
|
|
||||||
Header: JSX.Element
|
|
||||||
accessor: string
|
|
||||||
}[]
|
|
||||||
) =>
|
|
||||||
variables.reduce<HeaderCell[]>((headers, v) => {
|
|
||||||
if (parsedBlocks.find((b) => b.accessor === v.name)) return headers
|
|
||||||
return [
|
|
||||||
...headers,
|
|
||||||
{
|
|
||||||
Header: (
|
|
||||||
<HStack minW={'150px'} maxW="500px">
|
|
||||||
<CodeIcon />
|
|
||||||
<Text>{v.name}</Text>
|
|
||||||
</HStack>
|
|
||||||
),
|
|
||||||
accessor: v.name,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}, [])
|
|
||||||
|
@ -1,122 +0,0 @@
|
|||||||
import { Block, ResultWithAnswers, Variable, VariableWithValue } from 'models'
|
|
||||||
import useSWRInfinite from 'swr/infinite'
|
|
||||||
import { stringify } from 'qs'
|
|
||||||
import { Answer } from 'db'
|
|
||||||
import { byId, isDefined, sendRequest } from 'utils'
|
|
||||||
import { fetcher } from 'services/utils'
|
|
||||||
|
|
||||||
const paginationLimit = 50
|
|
||||||
|
|
||||||
const getKey = (
|
|
||||||
typebotId: string,
|
|
||||||
pageIndex: number,
|
|
||||||
previousPageData: {
|
|
||||||
results: ResultWithAnswers[]
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
if (previousPageData && previousPageData.results.length === 0) return null
|
|
||||||
if (pageIndex === 0) return `/api/typebots/${typebotId}/results?limit=50`
|
|
||||||
return `/api/typebots/${typebotId}/results?lastResultId=${
|
|
||||||
previousPageData.results[previousPageData.results.length - 1].id
|
|
||||||
}&limit=${paginationLimit}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useResults = ({
|
|
||||||
typebotId,
|
|
||||||
onError,
|
|
||||||
}: {
|
|
||||||
typebotId: string
|
|
||||||
onError: (error: Error) => void
|
|
||||||
}) => {
|
|
||||||
const { data, error, mutate, setSize, size, isValidating } = useSWRInfinite<
|
|
||||||
{ results: ResultWithAnswers[] },
|
|
||||||
Error
|
|
||||||
>(
|
|
||||||
(
|
|
||||||
pageIndex: number,
|
|
||||||
previousPageData: {
|
|
||||||
results: ResultWithAnswers[]
|
|
||||||
}
|
|
||||||
) => getKey(typebotId, pageIndex, previousPageData),
|
|
||||||
fetcher,
|
|
||||||
{ revalidateAll: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
if (error) onError(error)
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
isLoading: !error && !data,
|
|
||||||
mutate,
|
|
||||||
setSize,
|
|
||||||
size,
|
|
||||||
hasMore:
|
|
||||||
isValidating ||
|
|
||||||
(data &&
|
|
||||||
data.length > 0 &&
|
|
||||||
data[data.length - 1].results.length > 0 &&
|
|
||||||
data.length === paginationLimit),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deleteResults = async (typebotId: string, ids: string[]) => {
|
|
||||||
const params = stringify(
|
|
||||||
{
|
|
||||||
ids,
|
|
||||||
},
|
|
||||||
{ indices: false }
|
|
||||||
)
|
|
||||||
return sendRequest({
|
|
||||||
url: `/api/typebots/${typebotId}/results?${params}`,
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deleteAllResults = async (typebotId: string) =>
|
|
||||||
sendRequest({
|
|
||||||
url: `/api/typebots/${typebotId}/results`,
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const getAllResults = async (typebotId: string) =>
|
|
||||||
sendRequest<{ results: ResultWithAnswers[] }>({
|
|
||||||
url: `/api/typebots/${typebotId}/results`,
|
|
||||||
method: 'GET',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const parseDateToReadable = (dateStr: string): string => {
|
|
||||||
const date = new Date(dateStr)
|
|
||||||
return (
|
|
||||||
date.toDateString().split(' ').slice(1, 3).join(' ') +
|
|
||||||
', ' +
|
|
||||||
date.toLocaleTimeString([], {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const convertResultsToTableData =
|
|
||||||
({ variables, blocks }: { variables: Variable[]; blocks: Block[] }) =>
|
|
||||||
(results: ResultWithAnswers[] | undefined) =>
|
|
||||||
(results ?? []).map((result) => ({
|
|
||||||
'Submitted at': parseDateToReadable(result.createdAt),
|
|
||||||
...[...result.answers, ...result.prefilledVariables].reduce<{
|
|
||||||
[key: string]: string
|
|
||||||
}>((o, answerOrVariable) => {
|
|
||||||
if ('blockId' in answerOrVariable) {
|
|
||||||
const answer = answerOrVariable as Answer
|
|
||||||
const key =
|
|
||||||
(answer.variableId
|
|
||||||
? variables.find(byId(answer.variableId))?.name
|
|
||||||
: blocks.find(byId(answer.blockId))?.title) ?? ''
|
|
||||||
return {
|
|
||||||
...o,
|
|
||||||
[key]: answer.content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const variable = answerOrVariable as VariableWithValue
|
|
||||||
if (isDefined(o[variable.id])) return o
|
|
||||||
const key = variables.find(byId(variable.id))?.name ?? ''
|
|
||||||
return { ...o, [key]: variable.value }
|
|
||||||
}, {}),
|
|
||||||
}))
|
|
246
apps/builder/services/typebots/results.tsx
Normal file
246
apps/builder/services/typebots/results.tsx
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import {
|
||||||
|
Block,
|
||||||
|
InputStep,
|
||||||
|
InputStepType,
|
||||||
|
ResultWithAnswers,
|
||||||
|
Variable,
|
||||||
|
VariableWithValue,
|
||||||
|
} from 'models'
|
||||||
|
import useSWRInfinite from 'swr/infinite'
|
||||||
|
import { stringify } from 'qs'
|
||||||
|
import { Answer } from 'db'
|
||||||
|
import { byId, isDefined, isInputStep, sendRequest } from 'utils'
|
||||||
|
import { fetcher } from 'services/utils'
|
||||||
|
import { HStack, Text } from '@chakra-ui/react'
|
||||||
|
import { CodeIcon, CalendarIcon } from 'assets/icons'
|
||||||
|
import { StepIcon } from 'components/editor/StepsSideBar/StepIcon'
|
||||||
|
|
||||||
|
const paginationLimit = 50
|
||||||
|
|
||||||
|
const getKey = (
|
||||||
|
typebotId: string,
|
||||||
|
pageIndex: number,
|
||||||
|
previousPageData: {
|
||||||
|
results: ResultWithAnswers[]
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
if (previousPageData && previousPageData.results.length === 0) return null
|
||||||
|
if (pageIndex === 0) return `/api/typebots/${typebotId}/results?limit=50`
|
||||||
|
return `/api/typebots/${typebotId}/results?lastResultId=${
|
||||||
|
previousPageData.results[previousPageData.results.length - 1].id
|
||||||
|
}&limit=${paginationLimit}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useResults = ({
|
||||||
|
typebotId,
|
||||||
|
onError,
|
||||||
|
}: {
|
||||||
|
typebotId: string
|
||||||
|
onError: (error: Error) => void
|
||||||
|
}) => {
|
||||||
|
const { data, error, mutate, setSize, size, isValidating } = useSWRInfinite<
|
||||||
|
{ results: ResultWithAnswers[] },
|
||||||
|
Error
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
pageIndex: number,
|
||||||
|
previousPageData: {
|
||||||
|
results: ResultWithAnswers[]
|
||||||
|
}
|
||||||
|
) => getKey(typebotId, pageIndex, previousPageData),
|
||||||
|
fetcher,
|
||||||
|
{ revalidateAll: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (error) onError(error)
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
isLoading: !error && !data,
|
||||||
|
mutate,
|
||||||
|
setSize,
|
||||||
|
size,
|
||||||
|
hasMore:
|
||||||
|
isValidating ||
|
||||||
|
(data &&
|
||||||
|
data.length > 0 &&
|
||||||
|
data[data.length - 1].results.length > 0 &&
|
||||||
|
data.length === paginationLimit),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteResults = async (typebotId: string, ids: string[]) => {
|
||||||
|
const params = stringify(
|
||||||
|
{
|
||||||
|
ids,
|
||||||
|
},
|
||||||
|
{ indices: false }
|
||||||
|
)
|
||||||
|
return sendRequest({
|
||||||
|
url: `/api/typebots/${typebotId}/results?${params}`,
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteAllResults = async (typebotId: string) =>
|
||||||
|
sendRequest({
|
||||||
|
url: `/api/typebots/${typebotId}/results`,
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getAllResults = async (typebotId: string) =>
|
||||||
|
sendRequest<{ results: ResultWithAnswers[] }>({
|
||||||
|
url: `/api/typebots/${typebotId}/results`,
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const parseDateToReadable = (dateStr: string): string => {
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
return (
|
||||||
|
date.toDateString().split(' ').slice(1, 3).join(' ') +
|
||||||
|
', ' +
|
||||||
|
date.toLocaleTimeString([], {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type HeaderCell = {
|
||||||
|
Header: JSX.Element
|
||||||
|
accessor: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResultHeaderCell = {
|
||||||
|
label: string
|
||||||
|
stepId?: string
|
||||||
|
stepType?: InputStepType
|
||||||
|
isLong?: boolean
|
||||||
|
variableId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseSubmissionsColumns = (
|
||||||
|
resultHeader: ResultHeaderCell[]
|
||||||
|
): HeaderCell[] =>
|
||||||
|
resultHeader.map((header) => ({
|
||||||
|
Header: (
|
||||||
|
<HStack minW={header.isLong ? '400px' : '150px'} maxW="500px">
|
||||||
|
<HeaderIcon header={header} />
|
||||||
|
<Text>{header.label}</Text>
|
||||||
|
</HStack>
|
||||||
|
),
|
||||||
|
accessor: header.label,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const HeaderIcon = ({ header }: { header: ResultHeaderCell }) =>
|
||||||
|
header.stepType ? (
|
||||||
|
<StepIcon type={header.stepType} />
|
||||||
|
) : header.variableId ? (
|
||||||
|
<CodeIcon />
|
||||||
|
) : (
|
||||||
|
<CalendarIcon />
|
||||||
|
)
|
||||||
|
|
||||||
|
export const parseResultHeader = ({
|
||||||
|
blocks,
|
||||||
|
variables,
|
||||||
|
}: {
|
||||||
|
blocks: Block[]
|
||||||
|
variables: Variable[]
|
||||||
|
}): ResultHeaderCell[] => {
|
||||||
|
const parsedBlocks = parseInputsResultHeader({ blocks, variables })
|
||||||
|
return [
|
||||||
|
{ label: 'Submitted at' },
|
||||||
|
...parsedBlocks,
|
||||||
|
...parseVariablesHeaders(variables, parsedBlocks),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseInputsResultHeader = ({
|
||||||
|
blocks,
|
||||||
|
variables,
|
||||||
|
}: {
|
||||||
|
blocks: Block[]
|
||||||
|
variables: Variable[]
|
||||||
|
}): ResultHeaderCell[] =>
|
||||||
|
(
|
||||||
|
blocks
|
||||||
|
.flatMap((b) =>
|
||||||
|
b.steps.map((s) => ({
|
||||||
|
...s,
|
||||||
|
blockTitle: b.title,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
.filter((step) => isInputStep(step)) as (InputStep & {
|
||||||
|
blockTitle: string
|
||||||
|
})[]
|
||||||
|
).reduce<ResultHeaderCell[]>((headers, inputStep) => {
|
||||||
|
if (
|
||||||
|
headers.find(
|
||||||
|
(h) =>
|
||||||
|
isDefined(h.variableId) &&
|
||||||
|
h.variableId ===
|
||||||
|
variables.find(byId(inputStep.options.variableId))?.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return headers
|
||||||
|
const matchedVariableName =
|
||||||
|
inputStep.options.variableId &&
|
||||||
|
variables.find(byId(inputStep.options.variableId))?.name
|
||||||
|
|
||||||
|
let label = matchedVariableName ?? inputStep.blockTitle
|
||||||
|
const totalPrevious = headers.filter((h) => h.label.includes(label)).length
|
||||||
|
if (totalPrevious > 0) label = label + ` (${totalPrevious})`
|
||||||
|
return [
|
||||||
|
...headers,
|
||||||
|
{
|
||||||
|
stepType: inputStep.type,
|
||||||
|
stepId: inputStep.id,
|
||||||
|
variableId: inputStep.options.variableId,
|
||||||
|
label,
|
||||||
|
isLong: 'isLong' in inputStep.options && inputStep.options.isLong,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const parseVariablesHeaders = (
|
||||||
|
variables: Variable[],
|
||||||
|
stepResultHeader: ResultHeaderCell[]
|
||||||
|
) =>
|
||||||
|
variables.reduce<ResultHeaderCell[]>((headers, v) => {
|
||||||
|
if (stepResultHeader.find((h) => h.variableId === v.id)) return headers
|
||||||
|
return [
|
||||||
|
...headers,
|
||||||
|
{
|
||||||
|
label: v.name,
|
||||||
|
variableId: v.id,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
export const convertResultsToTableData = (
|
||||||
|
results: ResultWithAnswers[] | undefined,
|
||||||
|
header: ResultHeaderCell[]
|
||||||
|
): { [key: string]: string }[] =>
|
||||||
|
(results ?? []).map((result) => ({
|
||||||
|
'Submitted at': parseDateToReadable(result.createdAt),
|
||||||
|
...[...result.answers, ...result.prefilledVariables].reduce<{
|
||||||
|
[key: string]: string
|
||||||
|
}>((o, answerOrVariable) => {
|
||||||
|
if ('blockId' in answerOrVariable) {
|
||||||
|
const answer = answerOrVariable as Answer
|
||||||
|
const key = answer.variableId
|
||||||
|
? header.find((h) => h.variableId === answer.variableId)?.label
|
||||||
|
: header.find((h) => h.stepId === answer.stepId)?.label
|
||||||
|
if (!key) return o
|
||||||
|
return {
|
||||||
|
...o,
|
||||||
|
[key]: answer.content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const variable = answerOrVariable as VariableWithValue
|
||||||
|
if (isDefined(o[variable.id])) return o
|
||||||
|
const key = header.find((h) => h.variableId === variable.id)?.label
|
||||||
|
if (!key) return o
|
||||||
|
return { ...o, [key]: variable.value }
|
||||||
|
}, {}),
|
||||||
|
}))
|
155
apps/viewer/playwright/fixtures/typebots/hugeBlock.json
Normal file
155
apps/viewer/playwright/fixtures/typebots/hugeBlock.json
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
{
|
||||||
|
"id": "cl0jrltqx0037601agzjiy7t4",
|
||||||
|
"createdAt": "2022-03-09T16:17:51.321Z",
|
||||||
|
"updatedAt": "2022-03-09T16:19:07.037Z",
|
||||||
|
"name": "My typebot",
|
||||||
|
"ownerId": "cl0cfi60r0000381a2bft9yis",
|
||||||
|
"publishedTypebotId": "dm12bh6hmEQemywn86osJD",
|
||||||
|
"folderId": null,
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "dqork4dJJZk3RgKYavBpRE",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"id": "u7Px8eD9MWXNJEBwxQwJCF",
|
||||||
|
"type": "start",
|
||||||
|
"label": "Start",
|
||||||
|
"blockId": "dqork4dJJZk3RgKYavBpRE",
|
||||||
|
"outgoingEdgeId": "b3XsreaqtWt4CrZZmCKDpa"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Start",
|
||||||
|
"graphCoordinates": { "x": 0, "y": 0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2Vrpgk5VP9BUo3vKtM5kws",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"id": "s6GezsfD612D1naKwvhDFgA",
|
||||||
|
"type": "text",
|
||||||
|
"blockId": "2Vrpgk5VP9BUo3vKtM5kws",
|
||||||
|
"content": {
|
||||||
|
"html": "<div>Hi what's your name?</div>",
|
||||||
|
"richText": [
|
||||||
|
{ "type": "p", "children": [{ "text": "Hi what's your name?" }] }
|
||||||
|
],
|
||||||
|
"plainText": "Hi what's your name?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sq3mPXUrugs5t6FoME3T4t4",
|
||||||
|
"type": "text input",
|
||||||
|
"blockId": "2Vrpgk5VP9BUo3vKtM5kws",
|
||||||
|
"options": {
|
||||||
|
"isLong": false,
|
||||||
|
"labels": {
|
||||||
|
"button": "Send",
|
||||||
|
"placeholder": "Type your answer..."
|
||||||
|
},
|
||||||
|
"variableId": "93BD32WVM5JEQ1nmWtr2S5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s57hbzfpG2sVvXefznVhbVB",
|
||||||
|
"type": "text",
|
||||||
|
"blockId": "2Vrpgk5VP9BUo3vKtM5kws",
|
||||||
|
"content": {
|
||||||
|
"html": "<div>How old are you?</div>",
|
||||||
|
"richText": [
|
||||||
|
{ "type": "p", "children": [{ "text": "How old are you?" }] }
|
||||||
|
],
|
||||||
|
"plainText": "How old are you?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s4fFn3s7nouQk88iJ3oLgx6",
|
||||||
|
"type": "number input",
|
||||||
|
"blockId": "2Vrpgk5VP9BUo3vKtM5kws",
|
||||||
|
"options": {
|
||||||
|
"labels": { "button": "Send", "placeholder": "Type a number..." }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "scR6MewJwkPNJzABYG8NEA4",
|
||||||
|
"type": "text",
|
||||||
|
"blockId": "2Vrpgk5VP9BUo3vKtM5kws",
|
||||||
|
"content": {
|
||||||
|
"html": "<div>Cool!</div>",
|
||||||
|
"richText": [{ "type": "p", "children": [{ "text": "Cool!" }] }],
|
||||||
|
"plainText": "Cool!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s5fo1s8UTyHQ7CfqC6MRxyW",
|
||||||
|
"type": "text",
|
||||||
|
"blockId": "2Vrpgk5VP9BUo3vKtM5kws",
|
||||||
|
"content": {
|
||||||
|
"html": "<div>Do you eat pizza?</div>",
|
||||||
|
"richText": [
|
||||||
|
{ "type": "p", "children": [{ "text": "Do you eat pizza?" }] }
|
||||||
|
],
|
||||||
|
"plainText": "Do you eat pizza?"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sxn8UjQ2MjEMuRjhkh7LWws",
|
||||||
|
"type": "choice input",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "wGKXMr4mfySw1HNThND2Xd",
|
||||||
|
"type": 0,
|
||||||
|
"stepId": "sxn8UjQ2MjEMuRjhkh7LWws",
|
||||||
|
"content": "Yes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "qzqzVqMeo6TDUdMYckLZmf",
|
||||||
|
"type": 0,
|
||||||
|
"stepId": "sxn8UjQ2MjEMuRjhkh7LWws",
|
||||||
|
"content": "No"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"blockId": "2Vrpgk5VP9BUo3vKtM5kws",
|
||||||
|
"options": { "buttonLabel": "Send", "isMultipleChoice": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Block #1",
|
||||||
|
"graphCoordinates": { "x": 386, "y": 108 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variables": [{ "id": "93BD32WVM5JEQ1nmWtr2S5", "name": "Name" }],
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"id": "b3XsreaqtWt4CrZZmCKDpa",
|
||||||
|
"to": { "blockId": "2Vrpgk5VP9BUo3vKtM5kws" },
|
||||||
|
"from": {
|
||||||
|
"stepId": "u7Px8eD9MWXNJEBwxQwJCF",
|
||||||
|
"blockId": "dqork4dJJZk3RgKYavBpRE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme": {
|
||||||
|
"chat": {
|
||||||
|
"inputs": {
|
||||||
|
"color": "#303235",
|
||||||
|
"backgroundColor": "#FFFFFF",
|
||||||
|
"placeholderColor": "#9095A0"
|
||||||
|
},
|
||||||
|
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
|
||||||
|
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
|
||||||
|
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
|
||||||
|
},
|
||||||
|
"general": { "font": "Open Sans", "background": { "type": "None" } }
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"general": {
|
||||||
|
"isBrandingEnabled": true,
|
||||||
|
"isNewResultOnRefreshEnabled": false
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
|
||||||
|
},
|
||||||
|
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
|
||||||
|
},
|
||||||
|
"publicId": "my-typebot-zjiy7t4",
|
||||||
|
"customDomain": null
|
||||||
|
}
|
23
apps/viewer/playwright/tests/hugeBlock.spec.ts
Normal file
23
apps/viewer/playwright/tests/hugeBlock.spec.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import test, { expect } from '@playwright/test'
|
||||||
|
import path from 'path'
|
||||||
|
import { importTypebotInDatabase } from '../services/database'
|
||||||
|
import { typebotViewer } from '../services/selectorUtils'
|
||||||
|
import { generate } from 'short-uuid'
|
||||||
|
|
||||||
|
test('should work as expected', async ({ page }) => {
|
||||||
|
const typebotId = generate()
|
||||||
|
await importTypebotInDatabase(
|
||||||
|
path.join(__dirname, '../fixtures/typebots/hugeBlock.json'),
|
||||||
|
{ id: typebotId, publicId: `${typebotId}-public` }
|
||||||
|
)
|
||||||
|
await page.goto(`/${typebotId}-public`)
|
||||||
|
await typebotViewer(page).locator('input').fill('Baptiste')
|
||||||
|
await typebotViewer(page).locator('input').press('Enter')
|
||||||
|
await typebotViewer(page).locator('input').fill('26')
|
||||||
|
await typebotViewer(page).locator('input').press('Enter')
|
||||||
|
await typebotViewer(page).locator('button >> text=Yes').click()
|
||||||
|
await page.goto(`http://localhost:3000/typebots/${typebotId}/results`)
|
||||||
|
await expect(page.locator('text=Baptiste')).toBeVisible()
|
||||||
|
await expect(page.locator('text=26')).toBeVisible()
|
||||||
|
await expect(page.locator('text=Yes')).toBeVisible()
|
||||||
|
})
|
Reference in New Issue
Block a user