2
0

(typebotLink) Better typebot link with merge option

Closes #675
This commit is contained in:
Baptiste Arnaud
2023-08-24 07:48:30 +02:00
parent 0acede92ef
commit ee3b94c35d
59 changed files with 1147 additions and 696 deletions

View File

@@ -180,7 +180,8 @@
"type": "Typebot link",
"options": {
"typebotId": "current",
"groupId": "vLUAPaxKwPF49iZhg4XZYa"
"groupId": "vLUAPaxKwPF49iZhg4XZYa",
"mergeResults": false
}
}
],
@@ -305,7 +306,8 @@
"groupId": "1GvxCAAEysxJMxrVngud3X",
"options": {
"groupId": "vLUAPaxKwPF49iZhg4XZYa",
"typebotId": "current"
"typebotId": "current",
"mergeResults": false
}
}
],

View File

@@ -282,7 +282,8 @@
"type": "Typebot link",
"options": {
"typebotId": "current",
"groupId": "cl96ns9qr00043b6ii07bo25o"
"groupId": "cl96ns9qr00043b6ii07bo25o",
"mergeResults": false
}
}
]

View File

@@ -618,7 +618,8 @@
"groupId": "cl1r15f68005f2e6dvdtal7cp",
"options": {
"groupId": "cl1r09bc6000h2e6dqml18p4p",
"typebotId": "current"
"typebotId": "current",
"mergeResults": false
}
}
],

View File

@@ -65,7 +65,8 @@ export const getLinkedTypebots = authenticatedProcedure
(typebotIds, block) =>
block.type === LogicBlockType.TYPEBOT_LINK &&
isDefined(block.options.typebotId) &&
!typebotIds.includes(block.options.typebotId)
!typebotIds.includes(block.options.typebotId) &&
block.options.mergeResults !== false
? [...typebotIds, block.options.typebotId]
: typebotIds,
[]

View File

@@ -1,10 +1,11 @@
import { Stack } from '@chakra-ui/react'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { TypebotLinkOptions } from '@typebot.io/schemas'
import { byId } from '@typebot.io/lib'
import { GroupsDropdown } from './GroupsDropdown'
import { TypebotsDropdown } from './TypebotsDropdown'
import { useEffect, useState } from 'react'
import { trpc } from '@/lib/trpc'
import { isNotEmpty } from '@typebot.io/lib'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
type Props = {
options: TypebotLinkOptions
@@ -12,21 +13,30 @@ type Props = {
}
export const TypebotLinkForm = ({ options, onOptionsChange }: Props) => {
const { linkedTypebots, typebot, save } = useTypebot()
const [linkedTypebotId, setLinkedTypebotId] = useState(options.typebotId)
const { typebot } = useTypebot()
const handleTypebotIdChange = async (
typebotId: string | 'current' | undefined
) => onOptionsChange({ ...options, typebotId })
) => onOptionsChange({ ...options, typebotId, groupId: undefined })
const { data: linkedTypebotData } = trpc.typebot.getTypebot.useQuery(
{
typebotId: options.typebotId as string,
},
{
enabled: isNotEmpty(options.typebotId) && options.typebotId !== 'current',
}
)
const handleGroupIdChange = (groupId: string | undefined) =>
onOptionsChange({ ...options, groupId })
useEffect(() => {
if (linkedTypebotId === options.typebotId) return
setLinkedTypebotId(options.typebotId)
save().then()
}, [linkedTypebotId, options.typebotId, save])
const updateMergeResults = (mergeResults: boolean) =>
onOptionsChange({ ...options, mergeResults })
const isCurrentTypebotSelected =
(typebot && options.typebotId === typebot.id) ||
options.typebotId === 'current'
return (
<Stack>
@@ -40,22 +50,30 @@ export const TypebotLinkForm = ({ options, onOptionsChange }: Props) => {
)}
{options.typebotId && (
<GroupsDropdown
key={options.typebotId}
groups={
typebot &&
(options.typebotId === typebot.id ||
options.typebotId === 'current')
typebot && isCurrentTypebotSelected
? typebot.groups
: linkedTypebots?.find(byId(options.typebotId))?.groups ?? []
: linkedTypebotData?.typebot?.groups ?? []
}
groupId={options.groupId}
onGroupIdSelected={handleGroupIdChange}
isLoading={
linkedTypebots === undefined &&
linkedTypebotData?.typebot === undefined &&
options.typebotId !== 'current' &&
typebot &&
typebot.id !== options.typebotId
}
/>
)}
{!isCurrentTypebotSelected && (
<SwitchWithLabel
label="Merge answers"
moreInfoContent="If enabled, the answers collected in the linked typebot will be merged with the results of the current typebot."
initialValue={options.mergeResults ?? true}
onCheckChange={updateMergeResults}
/>
)}
</Stack>
)
}

View File

@@ -2,24 +2,36 @@ import { TypebotLinkBlock } from '@typebot.io/schemas'
import React from 'react'
import { Tag, Text } from '@chakra-ui/react'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { byId } from '@typebot.io/lib'
import { byId, isNotEmpty } from '@typebot.io/lib'
import { trpc } from '@/lib/trpc'
type Props = {
block: TypebotLinkBlock
}
export const TypebotLinkNode = ({ block }: Props) => {
const { linkedTypebots, typebot } = useTypebot()
const { typebot } = useTypebot()
const { data: linkedTypebotData } = trpc.typebot.getTypebot.useQuery(
{
typebotId: block.options.typebotId as string,
},
{
enabled:
isNotEmpty(block.options.typebotId) &&
block.options.typebotId !== 'current',
}
)
const isCurrentTypebot =
typebot &&
(block.options.typebotId === typebot.id ||
block.options.typebotId === 'current')
const linkedTypebot = isCurrentTypebot
? typebot
: linkedTypebots?.find(byId(block.options.typebotId))
const linkedTypebot = isCurrentTypebot ? typebot : linkedTypebotData?.typebot
const blockTitle = linkedTypebot?.groups.find(
byId(block.options.groupId)
)?.title
if (!block.options.typebotId)
return <Text color="gray.500">Configure...</Text>
return (

View File

@@ -61,7 +61,17 @@ export const TypebotsDropdown = ({
aria-label="Navigate to typebot"
icon={<ExternalLinkIcon />}
as={Link}
href={`/typebots/${typebotId}/edit?parentId=${query.typebotId}`}
href={{
pathname: '/typebots/[typebotId]/edit',
query: {
typebotId,
parentId: query.parentId
? Array.isArray(query.parentId)
? query.parentId.concat(query.typebotId?.toString() ?? '')
: [query.parentId, query.typebotId?.toString() ?? '']
: query.typebotId ?? [],
},
}}
/>
)}
</HStack>

View File

@@ -156,13 +156,22 @@ export const TypebotHeader = () => {
as={Link}
aria-label="Navigate back"
icon={<ChevronLeftIcon fontSize={25} />}
href={
router.query.parentId
? `/typebots/${router.query.parentId}/edit`
href={{
pathname: router.query.parentId
? '/typebots/[typebotId]/edit'
: typebot?.folderId
? `/typebots/folders/${typebot.folderId}`
: '/typebots'
}
? '/typebots/folders/[folderId]'
: '/typebots',
query: {
folderId: typebot?.folderId ?? [],
parentId: Array.isArray(router.query.parentId)
? router.query.parentId.slice(0, -1)
: [],
typebotId: Array.isArray(router.query.parentId)
? [...router.query.parentId].pop()
: router.query.parentId ?? [],
},
}}
size="sm"
/>
<HStack spacing={1}>

View File

@@ -1,4 +1,4 @@
import { LogicBlockType, PublicTypebot, Typebot } from '@typebot.io/schemas'
import { PublicTypebot, Typebot } from '@typebot.io/schemas'
import { Router, useRouter } from 'next/router'
import {
createContext,
@@ -49,7 +49,6 @@ const typebotContext = createContext<
{
typebot?: Typebot
publishedTypebot?: PublicTypebot
linkedTypebots?: Pick<Typebot, 'id' | 'groups' | 'variables' | 'name'>[]
isReadOnly?: boolean
isPublished: boolean
isSavingLoading: boolean
@@ -132,36 +131,6 @@ export const TypebotProvider = ({
{ redo, undo, flush, canRedo, canUndo, set: setLocalTypebot },
] = useUndo<Typebot>(undefined)
const linkedTypebotIds = useMemo(
() =>
typebot?.groups
.flatMap((group) => group.blocks)
.reduce<string[]>(
(typebotIds, block) =>
block.type === LogicBlockType.TYPEBOT_LINK &&
isDefined(block.options.typebotId) &&
!typebotIds.includes(block.options.typebotId)
? [...typebotIds, block.options.typebotId]
: typebotIds,
[]
) ?? [],
[typebot?.groups]
)
const { data: linkedTypebotsData } = trpc.getLinkedTypebots.useQuery(
{
typebotId: typebot?.id as string,
},
{
enabled: isDefined(typebot?.id) && linkedTypebotIds.length > 0,
onError: (error) =>
showToast({
title: 'Error while fetching linkedTypebots',
description: error.message,
}),
}
)
useEffect(() => {
if (!typebot && isDefined(localTypebot)) setLocalTypebot(undefined)
if (isFetchingTypebot) return
@@ -270,7 +239,6 @@ export const TypebotProvider = ({
value={{
typebot: localTypebot,
publishedTypebot,
linkedTypebots: linkedTypebotsData?.typebots ?? [],
isReadOnly: typebotData?.isReadOnly,
isSavingLoading: isSaving,
save: saveTypebot,

View File

@@ -1,11 +1,17 @@
import { useToast } from '@/hooks/useToast'
import { ResultHeaderCell, ResultWithAnswers } from '@typebot.io/schemas'
import {
LogicBlockType,
ResultHeaderCell,
ResultWithAnswers,
} from '@typebot.io/schemas'
import { createContext, ReactNode, useContext, useMemo } from 'react'
import { parseResultHeader } from '@typebot.io/lib/results'
import { useTypebot } from '../editor/providers/TypebotProvider'
import { useResultsQuery } from './hooks/useResultsQuery'
import { TableData } from './types'
import { convertResultsToTableData } from './helpers/convertResultsToTableData'
import { trpc } from '@/lib/trpc'
import { isDefined } from '@typebot.io/lib/utils'
const resultsContext = createContext<{
resultsList: { results: ResultWithAnswers[] }[] | undefined
@@ -32,7 +38,7 @@ export const ResultsProvider = ({
totalResults: number
onDeleteResults: (totalResultsDeleted: number) => void
}) => {
const { publishedTypebot, linkedTypebots } = useTypebot()
const { publishedTypebot } = useTypebot()
const { showToast } = useToast()
const { data, fetchNextPage, hasNextPage, refetch } = useResultsQuery({
typebotId,
@@ -41,6 +47,29 @@ export const ResultsProvider = ({
},
})
const linkedTypebotIds =
publishedTypebot?.groups
.flatMap((group) => group.blocks)
.reduce<string[]>(
(typebotIds, block) =>
block.type === LogicBlockType.TYPEBOT_LINK &&
isDefined(block.options.typebotId) &&
!typebotIds.includes(block.options.typebotId) &&
block.options.mergeResults !== false
? [...typebotIds, block.options.typebotId]
: typebotIds,
[]
) ?? []
const { data: linkedTypebotsData } = trpc.getLinkedTypebots.useQuery(
{
typebotId,
},
{
enabled: linkedTypebotIds.length > 0,
}
)
const flatResults = useMemo(
() => data?.flatMap((d) => d.results) ?? [],
[data]
@@ -49,9 +78,9 @@ export const ResultsProvider = ({
const resultHeader = useMemo(
() =>
publishedTypebot
? parseResultHeader(publishedTypebot, linkedTypebots)
? parseResultHeader(publishedTypebot, linkedTypebotsData?.typebots)
: [],
[linkedTypebots, publishedTypebot]
[linkedTypebotsData?.typebots, publishedTypebot]
)
const tableData = useMemo(

View File

@@ -23,6 +23,7 @@ import { useResults } from '../../ResultsProvider'
import { parseColumnOrder } from '../../helpers/parseColumnsOrder'
import { convertResultsToTableData } from '../../helpers/convertResultsToTableData'
import { parseAccessor } from '../../helpers/parseAccessor'
import { isDefined } from '@typebot.io/lib'
type Props = {
isOpen: boolean
@@ -30,7 +31,7 @@ type Props = {
}
export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => {
const { typebot, publishedTypebot, linkedTypebots } = useTypebot()
const { typebot, publishedTypebot } = useTypebot()
const workspaceId = typebot?.workspaceId
const typebotId = typebot?.id
const { showToast } = useToast()
@@ -41,6 +42,15 @@ export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => {
const [areDeletedBlocksIncluded, setAreDeletedBlocksIncluded] =
useState(false)
const { data: linkedTypebotsData } = trpc.getLinkedTypebots.useQuery(
{
typebotId: typebotId as string,
},
{
enabled: isDefined(typebotId),
}
)
const getAllResults = async () => {
if (!workspaceId || !typebotId) return []
const allResults = []
@@ -71,7 +81,11 @@ export const ExportAllResultsModal = ({ isOpen, onClose }: Props) => {
const results = await getAllResults()
const resultHeader = areDeletedBlocksIncluded
? parseResultHeader(publishedTypebot, linkedTypebots, results)
? parseResultHeader(
publishedTypebot,
linkedTypebotsData?.typebots,
results
)
: existingResultHeader
const dataToUnparse = convertResultsToTableData(results, resultHeader)

View File

@@ -43,6 +43,7 @@ import {
LogicBlockType,
defaultAbTestOptions,
BlockWithItems,
defaultTypebotLinkOptions,
} from '@typebot.io/schemas'
import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
@@ -122,7 +123,7 @@ const parseDefaultBlockOptions = (type: BlockWithOptionsType): BlockOptions => {
case LogicBlockType.JUMP:
return {}
case LogicBlockType.TYPEBOT_LINK:
return {}
return defaultTypebotLinkOptions
case LogicBlockType.AB_TEST:
return defaultAbTestOptions
case IntegrationBlockType.GOOGLE_SHEETS: