2
0

♻️ Introduce typebot v6 with events (#1013)

Closes #885
This commit is contained in:
Baptiste Arnaud
2023-11-08 15:34:16 +01:00
committed by GitHub
parent 68e4fc71fb
commit 35300eaf34
634 changed files with 58971 additions and 31449 deletions

View File

@@ -1,3 +1,10 @@
.slate-inline-code {
background-color: #805ad5;
color: white;
padding: 0.125rem 0.25rem;
border-radius: 0.35rem;
}
.slate-variable {
background-color: #ff8b1a;
color: #ffffff;

View File

@@ -10,7 +10,7 @@ import {
Stack,
} from '@chakra-ui/react'
import { ChevronLeftIcon } from '@/components/icons'
import React, { ReactNode } from 'react'
import React from 'react'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Props<T extends readonly any[]> = {
@@ -42,7 +42,7 @@ export const DropdownList = <T extends readonly any[]>({
{...props}
>
<chakra.span noOfLines={1} display="block">
{(currentItem ?? placeholder) as unknown as ReactNode}
{currentItem ?? placeholder}
</chakra.span>
</MenuButton>
<Portal>
@@ -57,7 +57,7 @@ export const DropdownList = <T extends readonly any[]>({
textOverflow="ellipsis"
onClick={handleMenuItemClick(item)}
>
{item as unknown as ReactNode}
{item}
</MenuItem>
))}
</Stack>

View File

@@ -11,6 +11,10 @@ import { TrashIcon, PlusIcon } from '@/components/icons'
import { createId } from '@paralleldrive/cuid2'
import React, { useEffect, useState } from 'react'
const defaultItem = {
id: createId(),
}
type ItemWithId<T> = T & { id: string }
export type TableListItemProps<T> = {
@@ -19,10 +23,11 @@ export type TableListItemProps<T> = {
}
type Props<T> = {
initialItems: ItemWithId<T>[]
initialItems?: ItemWithId<T>[]
isOrdered?: boolean
addLabel?: string
newItemDefaultProps?: Partial<T>
hasDefaultItem?: boolean
Item: (props: TableListItemProps<T>) => JSX.Element
ComponentBetweenItems?: (props: unknown) => JSX.Element
onItemsChange: (items: ItemWithId<T>[]) => void
@@ -33,15 +38,19 @@ export const TableList = <T,>({
isOrdered,
addLabel = 'Add',
newItemDefaultProps,
hasDefaultItem,
Item,
ComponentBetweenItems,
onItemsChange,
}: Props<T>) => {
const [items, setItems] = useState(initialItems)
const [items, setItems] = useState(
initialItems ?? hasDefaultItem ? ([defaultItem] as ItemWithId<T>[]) : []
)
const [showDeleteIndex, setShowDeleteIndex] = useState<number | null>(null)
useEffect(() => {
if (items.length && initialItems.length === 0) setItems(initialItems)
if (items.length && initialItems && initialItems?.length === 0)
setItems(initialItems)
}, [initialItems, items.length])
const createItem = () => {

View File

@@ -9,9 +9,8 @@ import {
Input,
HStack,
FormControl,
FormLabel,
} from '@chakra-ui/react'
import { useState, useRef, useEffect, ReactNode } from 'react'
import { useState, useRef, useEffect } from 'react'
import { useDebouncedCallback } from 'use-debounce'
import { isDefined } from '@typebot.io/lib'
import { useOutsideClick } from '@/hooks/useOutsideClick'
@@ -20,7 +19,6 @@ import { VariablesButton } from '@/features/variables/components/VariablesButton
import { Variable } from '@typebot.io/schemas'
import { injectVariableInText } from '@/features/variables/helpers/injectVariableInTextInput'
import { focusInput } from '@/helpers/focusInput'
import { MoreInfoTooltip } from '../MoreInfoTooltip'
import { env } from '@typebot.io/env'
type Props = {
@@ -30,7 +28,6 @@ type Props = {
debounceTimeout?: number
placeholder?: string
withVariableButton?: boolean
label?: ReactNode
moreInfoTooltip?: string
isRequired?: boolean
onChange: (value: string) => void
@@ -44,8 +41,6 @@ export const AutocompleteInput = ({
withVariableButton = true,
value,
defaultValue,
label,
moreInfoTooltip,
isRequired,
}: Props) => {
const bg = useColorModeValue('gray.200', 'gray.700')
@@ -161,14 +156,6 @@ export const AutocompleteInput = ({
return (
<FormControl isRequired={isRequired}>
{label && (
<FormLabel>
{label}{' '}
{moreInfoTooltip && (
<MoreInfoTooltip>{moreInfoTooltip}</MoreInfoTooltip>
)}
</FormLabel>
)}
<HStack ref={dropdownRef} spacing={0} w="full">
<Popover
isOpen={isOpen}

View File

@@ -168,7 +168,7 @@ export const Select = <T extends Item>({
pr={selectedItem ? 16 : 8}
w="full"
>
{!isTouched && (
{!isTouched && items && (
<Text noOfLines={1} data-testid="selected-item-label">
{inputValue}
</Text>

View File

@@ -19,12 +19,8 @@ export const userContext = createContext<{
updateUser: (newUser: Partial<User>) => void
}>({
isLoading: false,
logOut: () => {
console.log('logOut not implemented')
},
updateUser: () => {
console.log('updateUser not implemented')
},
logOut: () => {},
updateUser: () => {},
})
const debounceTimeout = 1000

View File

@@ -1,12 +1,13 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { PublicTypebot } from '@typebot.io/schemas'
import { z } from 'zod'
import { canReadTypebots } from '@/helpers/databaseRules'
import { totalAnswersInBlock } from '@typebot.io/schemas/features/analytics'
import { totalAnswersSchema } from '@typebot.io/schemas/features/analytics'
import { parseGroups } from '@typebot.io/schemas'
import { isInputBlock } from '@typebot.io/lib'
export const getTotalAnswersInBlocks = authenticatedProcedure
export const getTotalAnswers = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
@@ -21,7 +22,7 @@ export const getTotalAnswersInBlocks = authenticatedProcedure
typebotId: z.string(),
})
)
.output(z.object({ totalAnswersInBlocks: z.array(totalAnswersInBlock) }))
.output(z.object({ totalAnswers: z.array(totalAnswersSchema) }))
.query(async ({ input: { typebotId }, ctx: { user } }) => {
const typebot = await prisma.typebot.findFirst({
where: canReadTypebots(typebotId, user),
@@ -33,17 +34,17 @@ export const getTotalAnswersInBlocks = authenticatedProcedure
message: 'Published typebot not found',
})
const publishedTypebot = typebot.publishedTypebot as PublicTypebot
const totalAnswersPerBlock = await prisma.answer.groupBy({
by: ['itemId', 'blockId'],
by: ['blockId'],
where: {
result: {
typebotId: typebot.publishedTypebot.typebotId,
},
blockId: {
in: publishedTypebot.groups.flatMap((group) =>
group.blocks.map((block) => block.id)
in: parseGroups(typebot.publishedTypebot.groups, {
typebotVersion: typebot.publishedTypebot.version,
}).flatMap((group) =>
group.blocks.filter(isInputBlock).map((block) => block.id)
),
},
},
@@ -51,10 +52,9 @@ export const getTotalAnswersInBlocks = authenticatedProcedure
})
return {
totalAnswersInBlocks: totalAnswersPerBlock.map((answer) => ({
blockId: answer.blockId,
itemId: answer.itemId ?? undefined,
total: answer._count._all,
totalAnswers: totalAnswersPerBlock.map((a) => ({
blockId: a.blockId,
total: a._count._all,
})),
}
})

View File

@@ -0,0 +1,55 @@
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { canReadTypebots } from '@/helpers/databaseRules'
import { totalVisitedEdgesSchema } from '@typebot.io/schemas'
export const getTotalVisitedEdges = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/typebots/{typebotId}/analytics/totalVisitedEdges',
protect: true,
summary: 'List total edges used in results',
tags: ['Analytics'],
},
})
.input(
z.object({
typebotId: z.string(),
})
)
.output(
z.object({
totalVisitedEdges: z.array(totalVisitedEdgesSchema),
})
)
.query(async ({ input: { typebotId }, ctx: { user } }) => {
const typebot = await prisma.typebot.findFirst({
where: canReadTypebots(typebotId, user),
select: { id: true },
})
if (!typebot?.id)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Published typebot not found',
})
const edges = await prisma.visitedEdge.groupBy({
by: ['edgeId'],
where: {
result: {
typebotId: typebot.id,
},
},
_count: { resultId: true },
})
return {
totalVisitedEdges: edges.map((e) => ({
edgeId: e.edgeId,
total: e._count.resultId,
})),
}
})

View File

@@ -1,6 +1,8 @@
import { router } from '@/helpers/server/trpc'
import { getTotalAnswersInBlocks } from './getTotalAnswersInBlocks'
import { getTotalAnswers } from './getTotalAnswers'
import { getTotalVisitedEdges } from './getTotalVisitedEdges'
export const analyticsRouter = router({
getTotalAnswersInBlocks,
getTotalAnswers,
getTotalVisitedEdges,
})

View File

@@ -15,20 +15,25 @@ import { GroupsCoordinatesProvider } from '@/features/graph/providers/GroupsCoor
import { useTranslate } from '@tolgee/react'
import { trpc } from '@/lib/trpc'
import { isDefined } from '@typebot.io/lib'
import { EventsCoordinatesProvider } from '@/features/graph/providers/EventsCoordinateProvider'
export const AnalyticsGraphContainer = ({ stats }: { stats?: Stats }) => {
const { t } = useTranslate()
const { isOpen, onOpen, onClose } = useDisclosure()
const { typebot, publishedTypebot } = useTypebot()
const { data } = trpc.analytics.getTotalAnswersInBlocks.useQuery(
const { data } = trpc.analytics.getTotalAnswers.useQuery(
{
typebotId: typebot?.id as string,
},
{ enabled: isDefined(publishedTypebot) }
)
const { data: edgesData } = trpc.analytics.getTotalVisitedEdges.useQuery(
{
typebotId: typebot?.id as string,
},
{ enabled: isDefined(publishedTypebot) }
)
const startBlockId = publishedTypebot?.groups
.find((group) => group.blocks.at(0)?.type === 'start')
?.blocks.at(0)?.id
return (
<Flex
@@ -44,28 +49,18 @@ export const AnalyticsGraphContainer = ({ stats }: { stats?: Stats }) => {
h="full"
justifyContent="center"
>
{publishedTypebot &&
data?.totalAnswersInBlocks &&
stats &&
startBlockId ? (
{publishedTypebot && stats ? (
<GraphProvider isReadOnly>
<GroupsCoordinatesProvider groups={publishedTypebot?.groups}>
<Graph
flex="1"
typebot={publishedTypebot}
onUnlockProPlanClick={onOpen}
totalAnswersInBlocks={
startBlockId
? [
{
blockId: startBlockId,
total: stats.totalViews,
},
...data.totalAnswersInBlocks,
]
: []
}
/>
<EventsCoordinatesProvider events={publishedTypebot?.events}>
<Graph
flex="1"
typebot={publishedTypebot}
onUnlockProPlanClick={onOpen}
totalAnswers={data?.totalAnswers}
totalVisitedEdges={edgesData?.totalVisitedEdges}
/>
</EventsCoordinatesProvider>
</GroupsCoordinatesProvider>
</GraphProvider>
) : (

View File

@@ -1,96 +0,0 @@
import { isInputBlock } from '@typebot.io/lib'
import { PublicTypebot } from '@typebot.io/schemas'
import { TotalAnswersInBlock } from '@typebot.io/schemas/features/analytics'
export const computePreviousTotalAnswers = (
publishedTypebot: PublicTypebot,
currentBlockId: string,
totalAnswersInBlocks: TotalAnswersInBlock[],
visitedBlocks: string[] = []
): number => {
let totalAnswers = 0
const allBlocks = publishedTypebot.groups.flatMap((group) => group.blocks)
const currentGroup = publishedTypebot.groups.find((group) =>
group.blocks.find((block) => block.id === currentBlockId)
)
if (!currentGroup) return 0
const currentBlockIndex = currentGroup.blocks.findIndex(
(block) => block.id === currentBlockId
)
const previousBlocks = currentGroup.blocks.slice(0, currentBlockIndex + 1)
for (const block of previousBlocks.reverse()) {
if (visitedBlocks.includes(block.id)) continue
if (
currentBlockId !== block.id &&
(isInputBlock(block) || block.type === 'start')
) {
visitedBlocks.push(block.id)
return (
totalAnswersInBlocks.find(
(totalAnswersInBlock) =>
totalAnswersInBlock.blockId === block.id &&
totalAnswersInBlock.itemId === undefined
)?.total ?? 0
)
}
const connectedEdges = publishedTypebot.edges.filter(
(edge) => edge.to.blockId === block.id
)
if (connectedEdges.length) {
for (const connectedEdge of connectedEdges) {
const connectedBlock = allBlocks.find(
(block) => block.id === connectedEdge.from.blockId
)
if (connectedBlock && !visitedBlocks.includes(connectedBlock.id)) {
if (isInputBlock(connectedBlock) || connectedBlock.type === 'start') {
visitedBlocks.push(connectedBlock.id)
totalAnswers +=
totalAnswersInBlocks.find(
(totalAnswersInBlock) =>
totalAnswersInBlock.blockId === connectedEdge.from.blockId &&
totalAnswersInBlock.itemId === connectedEdge.from.itemId
)?.total ?? 0
} else {
totalAnswers += computePreviousTotalAnswers(
publishedTypebot,
connectedBlock.id,
totalAnswersInBlocks,
visitedBlocks
)
}
}
}
}
}
const edgesConnectedToGroup = publishedTypebot.edges.filter(
(edge) => edge.to.groupId === currentGroup.id
)
if (edgesConnectedToGroup.length) {
for (const connectedEdge of edgesConnectedToGroup) {
const connectedBlock = allBlocks.find(
(block) => block.id === connectedEdge.from.blockId
)
if (connectedBlock && !visitedBlocks.includes(connectedBlock.id)) {
if (isInputBlock(connectedBlock) || connectedBlock.type === 'start') {
visitedBlocks.push(connectedBlock.id)
totalAnswers +=
totalAnswersInBlocks.find(
(totalAnswersInBlock) =>
totalAnswersInBlock.blockId === connectedEdge.from.blockId &&
totalAnswersInBlock.itemId === connectedEdge.from.itemId
)?.total ?? 0
} else {
totalAnswers += computePreviousTotalAnswers(
publishedTypebot,
connectedBlock.id,
totalAnswersInBlocks,
visitedBlocks
)
}
}
}
}
return totalAnswers
}

View File

@@ -0,0 +1,60 @@
import { isInputBlock, isNotDefined } from '@typebot.io/lib'
import { PublicTypebotV6 } from '@typebot.io/schemas'
import {
TotalAnswers,
TotalVisitedEdges,
} from '@typebot.io/schemas/features/analytics'
export const computeTotalUsersAtBlock = (
currentBlockId: string,
{
publishedTypebot,
totalVisitedEdges,
totalAnswers,
}: {
publishedTypebot: PublicTypebotV6
totalVisitedEdges: TotalVisitedEdges[]
totalAnswers: TotalAnswers[]
}
): number => {
let totalUsers = 0
const currentGroup = publishedTypebot.groups.find((group) =>
group.blocks.find((block) => block.id === currentBlockId)
)
if (!currentGroup) return 0
const currentBlockIndex = currentGroup.blocks.findIndex(
(block) => block.id === currentBlockId
)
const previousBlocks = currentGroup.blocks.slice(0, currentBlockIndex + 1)
for (const block of previousBlocks.reverse()) {
if (currentBlockId !== block.id && isInputBlock(block))
return totalAnswers.find((t) => t.blockId === block.id)?.total ?? 0
const incomingEdges = publishedTypebot.edges.filter(
(edge) => edge.to.blockId === block.id
)
if (!incomingEdges.length) continue
totalUsers += incomingEdges.reduce(
(acc, incomingEdge) =>
acc +
(totalVisitedEdges.find(
(totalEdge) => totalEdge.edgeId === incomingEdge.id
)?.total ?? 0),
0
)
}
const edgesConnectedToGroup = publishedTypebot.edges.filter(
(edge) =>
edge.to.groupId === currentGroup.id && isNotDefined(edge.to.blockId)
)
totalUsers += edgesConnectedToGroup.reduce(
(acc, connectedEdge) =>
acc +
(totalVisitedEdges.find(
(totalEdge) => totalEdge.edgeId === connectedEdge.id
)?.total ?? 0),
0
)
return totalUsers
}

View File

@@ -0,0 +1,20 @@
import { byId } from '@typebot.io/lib'
import { PublicTypebotV6 } from '@typebot.io/schemas'
import { TotalAnswers } from '@typebot.io/schemas/features/analytics'
export const getTotalAnswersAtBlock = (
currentBlockId: string,
{
publishedTypebot,
totalAnswers,
}: {
publishedTypebot: PublicTypebotV6
totalAnswers: TotalAnswers[]
}
): number => {
const block = publishedTypebot.groups
.flatMap((g) => g.blocks)
.find(byId(currentBlockId))
if (!block) throw new Error(`Block ${currentBlockId} not found`)
return totalAnswers.find((t) => t.blockId === block.id)?.total ?? 0
}

View File

@@ -1,10 +1,10 @@
import test, { expect } from '@playwright/test'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import { BubbleBlockType, defaultAudioBubbleContent } from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { getTestAsset } from '@/test/utils/playwright'
import { proWorkspaceId } from '@typebot.io/lib/playwright/databaseSetup'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
const audioSampleUrl =
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'
@@ -16,7 +16,6 @@ test('should work as expected', async ({ page }) => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: BubbleBlockType.AUDIO,
content: defaultAudioBubbleContent,
}),
},
])

View File

@@ -1,16 +1,17 @@
import { Button, Flex, HStack, Stack, Text } from '@chakra-ui/react'
import { AudioBubbleContent } from '@typebot.io/schemas'
import { TextInput } from '@/components/inputs'
import { useState } from 'react'
import { UploadButton } from '@/components/ImageUploadContent/UploadButton'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { useTranslate } from '@tolgee/react'
import { FilePathUploadProps } from '@/features/upload/api/generateUploadUrl'
import { AudioBubbleBlock } from '@typebot.io/schemas'
import { defaultAudioBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/audio/constants'
type Props = {
uploadFileProps: FilePathUploadProps
content: AudioBubbleContent
onContentChange: (content: AudioBubbleContent) => void
content: AudioBubbleBlock['content']
onContentChange: (content: AudioBubbleBlock['content']) => void
}
export const AudioBubbleForm = ({
@@ -64,7 +65,7 @@ export const AudioBubbleForm = ({
placeholder={t(
'editor.blocks.bubbles.audio.settings.worksWith.placeholder'
)}
defaultValue={content.url ?? ''}
defaultValue={content?.url ?? ''}
onChange={updateUrl}
/>
<Text fontSize="sm" color="gray.400" textAlign="center">
@@ -75,7 +76,10 @@ export const AudioBubbleForm = ({
</Stack>
<SwitchWithLabel
label={t('editor.blocks.bubbles.audio.settings.autoplay.label')}
initialValue={content.isAutoplayEnabled ?? true}
initialValue={
content?.isAutoplayEnabled ??
defaultAudioBubbleContent.isAutoplayEnabled
}
onCheckChange={updateAutoPlay}
/>
</Stack>

View File

@@ -1,10 +1,10 @@
import { Text } from '@chakra-ui/react'
import { AudioBubbleContent } from '@typebot.io/schemas'
import { isDefined } from '@typebot.io/lib'
import { useTranslate } from '@tolgee/react'
import { AudioBubbleBlock } from '@typebot.io/schemas'
type Props = {
url: AudioBubbleContent['url']
url: NonNullable<AudioBubbleBlock['content']>['url']
}
export const AudioBubbleNode = ({ url }: Props) => {

View File

@@ -1,12 +1,13 @@
import { TextInput, NumberInput } from '@/components/inputs'
import { Stack, Text } from '@chakra-ui/react'
import { EmbedBubbleContent } from '@typebot.io/schemas'
import { EmbedBubbleBlock } from '@typebot.io/schemas'
import { sanitizeUrl } from '@typebot.io/lib'
import { useTranslate } from '@tolgee/react'
import { defaultEmbedBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/embed/constants'
type Props = {
content: EmbedBubbleContent
onSubmit: (content: EmbedBubbleContent) => void
content: EmbedBubbleBlock['content']
onSubmit: (content: EmbedBubbleBlock['content']) => void
}
export const EmbedUploadContent = ({ content, onSubmit }: Props) => {
@@ -18,8 +19,9 @@ export const EmbedUploadContent = ({ content, onSubmit }: Props) => {
onSubmit({ ...content, url: iframeUrl })
}
const handleHeightChange = (height?: EmbedBubbleContent['height']) =>
height && onSubmit({ ...content, height })
const handleHeightChange = (
height?: NonNullable<EmbedBubbleBlock['content']>['height']
) => height && onSubmit({ ...content, height })
return (
<Stack p="2" spacing={6}>
@@ -38,7 +40,7 @@ export const EmbedUploadContent = ({ content, onSubmit }: Props) => {
<NumberInput
label="Height:"
defaultValue={content?.height}
defaultValue={content?.height ?? defaultEmbedBubbleContent.height}
onValueChange={handleHeightChange}
suffix={t('editor.blocks.bubbles.embed.settings.numberInput.unit')}
width="150px"

View File

@@ -1,8 +1,8 @@
import test, { expect } from '@playwright/test'
import { BubbleBlockType, defaultEmbedBubbleContent } from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
const pdfSrc = 'https://www.orimi.com/pdf-test.pdf'
const siteSrc = 'https://app.cal.com/baptistearno/15min'
@@ -16,7 +16,6 @@ test.describe.parallel('Embed bubble block', () => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: BubbleBlockType.EMBED,
content: defaultEmbedBubbleContent,
}),
},
])

View File

@@ -7,6 +7,7 @@ import { Stack } from '@chakra-ui/react'
import { isDefined, isNotEmpty } from '@typebot.io/lib'
import { ImageBubbleBlock } from '@typebot.io/schemas'
import React, { useState } from 'react'
import { defaultImageBubbleContent } from '@typebot.io/schemas/features/blocks/bubbles/image/constants'
type Props = {
uploadFileProps: FilePathUploadProps
@@ -21,7 +22,7 @@ export const ImageBubbleSettings = ({
}: Props) => {
const { t } = useTranslate()
const [showClickLinkInput, setShowClickLinkInput] = useState(
isNotEmpty(block.content.clickLink?.url)
isNotEmpty(block.content?.clickLink?.url)
)
const updateImage = (url: string) => {
@@ -31,19 +32,19 @@ export const ImageBubbleSettings = ({
const updateClickLinkUrl = (url: string) => {
onContentChange({
...block.content,
clickLink: { ...block.content.clickLink, url },
clickLink: { ...block.content?.clickLink, url },
})
}
const updateClickLinkAltText = (alt: string) => {
onContentChange({
...block.content,
clickLink: { ...block.content.clickLink, alt },
clickLink: { ...block.content?.clickLink, alt },
})
}
const toggleClickLink = () => {
if (isDefined(block.content.clickLink) && showClickLinkInput) {
if (isDefined(block.content?.clickLink) && showClickLinkInput) {
onContentChange({ ...block.content, clickLink: undefined })
}
setShowClickLinkInput(!showClickLinkInput)
@@ -55,6 +56,7 @@ export const ImageBubbleSettings = ({
uploadFileProps={uploadFileProps}
defaultUrl={block.content?.url}
onSubmit={updateImage}
excludedTabs={['emoji']}
/>
<Stack>
<SwitchWithLabel
@@ -68,14 +70,17 @@ export const ImageBubbleSettings = ({
autoFocus
placeholder="https://example.com"
onChange={updateClickLinkUrl}
defaultValue={block.content.clickLink?.url}
defaultValue={block.content?.clickLink?.url}
/>
<TextInput
placeholder={t(
'editor.blocks.bubbles.image.switchWithLabel.onClick.placeholder'
)}
onChange={updateClickLinkAltText}
defaultValue={block.content.clickLink?.alt}
defaultValue={
block.content?.clickLink?.alt ??
defaultImageBubbleContent.clickLink.alt
}
/>
</>
)}

View File

@@ -1,10 +1,10 @@
import test, { expect } from '@playwright/test'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import { BubbleBlockType, defaultImageBubbleContent } from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { getTestAsset } from '@/test/utils/playwright'
import { proWorkspaceId } from '@typebot.io/lib/playwright/databaseSetup'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
const unsplashImageSrc =
'https://images.unsplash.com/photo-1504297050568-910d24c426d3?ixlib=rb-1.2.1&ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1287&q=80'
@@ -18,7 +18,6 @@ test.describe.parallel('Image bubble block', () => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: BubbleBlockType.IMAGE,
content: defaultImageBubbleContent,
}),
},
])
@@ -44,7 +43,6 @@ test.describe.parallel('Image bubble block', () => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: BubbleBlockType.IMAGE,
content: defaultImageBubbleContent,
}),
},
])
@@ -66,7 +64,6 @@ test.describe.parallel('Image bubble block', () => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: BubbleBlockType.IMAGE,
content: defaultImageBubbleContent,
}),
},
])

View File

@@ -8,7 +8,7 @@ type Props = {
}
export const TextBubbleContent = ({ block }: Props) => {
const isEmpty = block.content.richText.length === 0
const isEmpty = (block.content?.richText?.length ?? 0) === 0
return (
<Flex
w="90%"
@@ -17,7 +17,7 @@ export const TextBubbleContent = ({ block }: Props) => {
className="slate-html-container"
color={isEmpty ? 'gray.500' : 'inherit'}
>
{block.content.richText.map((element, idx) => (
{block.content?.richText?.map((element, idx) => (
<PlateBlock key={idx} element={element} />
))}
</Flex>

View File

@@ -26,22 +26,32 @@ export const PlateText = ({
const PlateTextContent = ({ text }: { text: string }) => {
const { typebot } = useTypebot()
return (
<>
{text.split(/\{\{(.*?\}\})/g).map((str, idx) => {
if (str.endsWith('}}')) {
const variableName = str.trim().slice(0, -2)
const matchingVariable = typebot?.variables.find(
(variable) => variable.name === variableName
)
if (!matchingVariable) return '{{' + str
{text.split(/\{\{=(.*?=\}\})/g).map((str, idx) => {
if (str.endsWith('=}}')) {
return (
<span className="slate-variable" key={idx}>
{str.trim().slice(0, -2)}
<span className="slate-inline-code" key={idx}>
{str.trim().slice(0, -3)}
</span>
)
}
return str
return str.split(/\{\{(.*?\}\})/g).map((str, idx) => {
if (str.endsWith('}}')) {
const variableName = str.trim().slice(0, -2)
const matchingVariable = typebot?.variables.find(
(variable) => variable.name === variableName
)
if (!matchingVariable) return '{{' + str
return (
<span className="slate-variable" key={idx}>
{str.trim().slice(0, -2)}
</span>
)
}
return str
})
})}
</>
)

View File

@@ -1,8 +1,8 @@
import test, { expect } from '@playwright/test'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import { BubbleBlockType, defaultTextBubbleContent } from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
test.describe('Text bubble block', () => {
test('rich text features should work', async ({ page }) => {
@@ -12,7 +12,6 @@ test.describe('Text bubble block', () => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: BubbleBlockType.TEXT,
content: defaultTextBubbleContent,
}),
},
])

View File

@@ -1,6 +1,7 @@
import { useTranslate } from '@tolgee/react'
import { Box, Text, Image } from '@chakra-ui/react'
import { VideoBubbleBlock, VideoBubbleContentType } from '@typebot.io/schemas'
import { VideoBubbleBlock } from '@typebot.io/schemas'
import { VideoBubbleContentType } from '@typebot.io/schemas/features/blocks/bubbles/video/constants'
type Props = {
block: VideoBubbleBlock

View File

@@ -1,16 +1,16 @@
import { Stack, Text } from '@chakra-ui/react'
import {
VariableString,
VideoBubbleContent,
VideoBubbleContentType,
} from '@typebot.io/schemas'
import { VariableString, VideoBubbleBlock } from '@typebot.io/schemas'
import { NumberInput, TextInput } from '@/components/inputs'
import { useTranslate } from '@tolgee/react'
import { parseVideoUrl } from '@typebot.io/lib/parseVideoUrl'
import {
VideoBubbleContentType,
defaultVideoBubbleContent,
} from '@typebot.io/schemas/features/blocks/bubbles/video/constants'
type Props = {
content?: VideoBubbleContent
onSubmit: (content: VideoBubbleContent) => void
content?: VideoBubbleBlock['content']
onSubmit: (content: VideoBubbleBlock['content']) => void
}
export const VideoUploadContent = ({ content, onSubmit }: Props) => {
@@ -47,7 +47,7 @@ export const VideoUploadContent = ({ content, onSubmit }: Props) => {
{content?.type !== VideoBubbleContentType.URL && (
<NumberInput
label="Height:"
defaultValue={content?.height ?? 400}
defaultValue={content?.height ?? defaultVideoBubbleContent.height}
onValueChange={updateHeight}
suffix={t('editor.blocks.bubbles.video.settings.numberInput.unit')}
width="150px"

View File

@@ -1,12 +1,9 @@
import test, { expect } from '@playwright/test'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import {
BubbleBlockType,
defaultVideoBubbleContent,
VideoBubbleContentType,
} from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
import { VideoBubbleContentType } from '@typebot.io/schemas/features/blocks/bubbles/video/constants'
const videoSrc =
'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4'
@@ -22,7 +19,6 @@ test.describe.parallel('Video bubble block', () => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: BubbleBlockType.VIDEO,
content: defaultVideoBubbleContent,
}),
},
])

View File

@@ -4,13 +4,9 @@ import {
importTypebotInDatabase,
} from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import {
defaultChoiceInputOptions,
InputBlockType,
ItemType,
} from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { getTestAsset } from '@/test/utils/playwright'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
test.describe.parallel('Buttons input block', () => {
test('can edit button items', async ({ page }) => {
@@ -23,11 +19,8 @@ test.describe.parallel('Buttons input block', () => {
items: [
{
id: 'choice1',
blockId: 'block1',
type: ItemType.BUTTON,
},
],
options: { ...defaultChoiceInputOptions },
}),
},
])

View File

@@ -13,12 +13,12 @@ type Props = {
export const ButtonsBlockNode = ({ block, indices }: Props) => {
const { typebot } = useTypebot()
const dynamicVariableName = typebot?.variables.find(
(variable) => variable.id === block.options.dynamicVariableId
(variable) => variable.id === block.options?.dynamicVariableId
)?.name
return (
<Stack w="full">
{block.options.dynamicVariableId ? (
{block.options?.dynamicVariableId ? (
<Wrap spacing={1}>
<Text>Display</Text>
<Tag bg="orange.400" color="white">
@@ -29,7 +29,7 @@ export const ButtonsBlockNode = ({ block, indices }: Props) => {
) : (
<ItemNodesList block={block} indices={indices} />
)}
{block.options.variableId ? (
{block.options?.variableId ? (
<SetVariableLabel
variableId={block.options.variableId}
variables={typebot?.variables}

View File

@@ -2,49 +2,53 @@ import { TextInput } from '@/components/inputs'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { FormControl, FormLabel, Stack } from '@chakra-ui/react'
import {
ChoiceInputOptions,
Variable,
defaultChoiceInputOptions,
} from '@typebot.io/schemas'
import { ChoiceInputBlock, Variable } from '@typebot.io/schemas'
import React from 'react'
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
import { defaultChoiceInputOptions } from '@typebot.io/schemas/features/blocks/inputs/choice/constants'
type Props = {
options?: ChoiceInputOptions
onOptionsChange: (options: ChoiceInputOptions) => void
options?: ChoiceInputBlock['options']
onOptionsChange: (options: ChoiceInputBlock['options']) => void
}
export const ButtonsBlockSettings = ({ options, onOptionsChange }: Props) => {
const updateIsMultiple = (isMultipleChoice: boolean) =>
options && onOptionsChange({ ...options, isMultipleChoice })
onOptionsChange({ ...options, isMultipleChoice })
const updateIsSearchable = (isSearchable: boolean) =>
options && onOptionsChange({ ...options, isSearchable })
onOptionsChange({ ...options, isSearchable })
const updateButtonLabel = (buttonLabel: string) =>
options && onOptionsChange({ ...options, buttonLabel })
onOptionsChange({ ...options, buttonLabel })
const updateSearchInputPlaceholder = (searchInputPlaceholder: string) =>
options && onOptionsChange({ ...options, searchInputPlaceholder })
onOptionsChange({ ...options, searchInputPlaceholder })
const updateSaveVariable = (variable?: Variable) =>
options && onOptionsChange({ ...options, variableId: variable?.id })
onOptionsChange({ ...options, variableId: variable?.id })
const updateDynamicDataVariable = (variable?: Variable) =>
options && onOptionsChange({ ...options, dynamicVariableId: variable?.id })
onOptionsChange({ ...options, dynamicVariableId: variable?.id })
return (
<Stack spacing={4}>
<SwitchWithRelatedSettings
label="Multiple choice?"
initialValue={options?.isMultipleChoice ?? false}
initialValue={
options?.isMultipleChoice ??
defaultChoiceInputOptions.isMultipleChoice
}
onCheckChange={updateIsMultiple}
>
<TextInput
label="Submit button label:"
defaultValue={options?.buttonLabel ?? 'Send'}
defaultValue={
options?.buttonLabel ?? defaultChoiceInputOptions.buttonLabel
}
onChange={updateButtonLabel}
/>
</SwitchWithRelatedSettings>
<SwitchWithRelatedSettings
label="Is searchable?"
initialValue={options?.isSearchable ?? false}
initialValue={
options?.isSearchable ?? defaultChoiceInputOptions.isSearchable
}
onCheckChange={updateIsSearchable}
>
<TextInput

View File

@@ -16,7 +16,7 @@ import {
} from '@chakra-ui/react'
import { PlusIcon, SettingsIcon } from '@/components/icons'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { ButtonItem, Item, ItemIndices, ItemType } from '@typebot.io/schemas'
import { ButtonItem, Item, ItemIndices } from '@typebot.io/schemas'
import React, { useRef, useState } from 'react'
import { isNotDefined } from '@typebot.io/lib'
import { useGraph } from '@/features/graph/providers/GraphProvider'
@@ -54,7 +54,7 @@ export const ButtonsItemNode = ({ item, indices, isMouseOver }: Props) => {
const handlePlusClick = () => {
const itemIndex = indices.itemIndex + 1
createItem({ type: ItemType.BUTTON }, { ...indices, itemIndex })
createItem({}, { ...indices, itemIndex })
}
const updateItemSettings = (settings: Omit<ButtonItem, 'content'>) => {

View File

@@ -2,7 +2,8 @@ import React from 'react'
import { Stack } from '@chakra-ui/react'
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
import { ConditionForm } from '@/features/blocks/logic/condition/components/ConditionForm'
import { ButtonItem, Condition, LogicalOperator } from '@typebot.io/schemas'
import { ButtonItem, Condition } from '@typebot.io/schemas'
import { LogicalOperator } from '@typebot.io/schemas/features/blocks/logic/condition/constants'
type Props = {
item: ButtonItem

View File

@@ -3,12 +3,13 @@ import { TextInput } from '@/components/inputs'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { FormLabel, Stack } from '@chakra-ui/react'
import { DateInputOptions, Variable } from '@typebot.io/schemas'
import { DateInputBlock, Variable } from '@typebot.io/schemas'
import React from 'react'
import { defaultDateInputOptions } from '@typebot.io/schemas/features/blocks/inputs/date/constants'
type Props = {
options: DateInputOptions
onOptionsChange: (options: DateInputOptions) => void
options: DateInputBlock['options']
onOptionsChange: (options: DateInputBlock['options']) => void
}
export const DateInputSettings = ({ options, onOptionsChange }: Props) => {
@@ -41,47 +42,56 @@ export const DateInputSettings = ({ options, onOptionsChange }: Props) => {
<Stack spacing={4}>
<SwitchWithRelatedSettings
label="Is range?"
initialValue={options.isRange}
initialValue={options?.isRange ?? defaultDateInputOptions.isRange}
onCheckChange={updateIsRange}
>
<TextInput
label="From label:"
defaultValue={options.labels.from}
defaultValue={options?.labels?.from}
onChange={updateFromLabel}
/>
<TextInput
label="To label:"
defaultValue={options.labels.to}
defaultValue={
options?.labels?.to ?? defaultDateInputOptions.labels.to
}
onChange={updateToLabel}
/>
</SwitchWithRelatedSettings>
<SwitchWithLabel
label="With time?"
initialValue={options.hasTime}
initialValue={options?.hasTime ?? defaultDateInputOptions.hasTime}
onCheckChange={updateHasTime}
/>
<TextInput
label="Button label:"
defaultValue={options.labels.button}
defaultValue={
options?.labels?.button ?? defaultDateInputOptions.labels.button
}
onChange={updateButtonLabel}
/>
<TextInput
label="Min:"
defaultValue={options.min}
placeholder={options.hasTime ? 'YYYY-MM-DDTHH:mm' : 'YYYY-MM-DD'}
defaultValue={options?.min}
placeholder={options?.hasTime ? 'YYYY-MM-DDTHH:mm' : 'YYYY-MM-DD'}
onChange={updateMin}
/>
<TextInput
label="Max:"
defaultValue={options.max}
placeholder={options.hasTime ? 'YYYY-MM-DDTHH:mm' : 'YYYY-MM-DD'}
defaultValue={options?.max}
placeholder={options?.hasTime ? 'YYYY-MM-DDTHH:mm' : 'YYYY-MM-DD'}
onChange={updateMax}
/>
<TextInput
label="Format:"
defaultValue={options.format}
moreInfoTooltip="Popular formats: dd/MM/yyyy, MM/dd/yy, yyyy-MM-dd"
placeholder={options.hasTime ? 'dd/MM/yyyy HH:mm' : 'dd/MM/yyyy'}
defaultValue={
options?.format ??
(options?.hasTime
? defaultDateInputOptions.formatWithTime
: defaultDateInputOptions.format)
}
moreInfoTooltip="i.e dd/MM/yyyy, MM/dd/yy, yyyy-MM-dd"
placeholder={options?.hasTime ? 'dd/MM/yyyy HH:mm' : 'dd/MM/yyyy'}
onChange={updateFormat}
/>
<Stack>
@@ -89,7 +99,7 @@ export const DateInputSettings = ({ options, onOptionsChange }: Props) => {
Save answer in a variable:
</FormLabel>
<VariableSearchInput
initialVariableId={options.variableId}
initialVariableId={options?.variableId}
onSelectVariable={updateVariable}
/>
</Stack>

View File

@@ -1,8 +1,8 @@
import test, { expect } from '@playwright/test'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import { defaultDateInputOptions, InputBlockType } from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
test.describe('Date input block', () => {
test('options should work', async ({ page }) => {
@@ -12,7 +12,6 @@ test.describe('Date input block', () => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: InputBlockType.DATE,
options: defaultDateInputOptions,
}),
},
])

View File

@@ -2,15 +2,19 @@ import React from 'react'
import { Text } from '@chakra-ui/react'
import { EmailInputBlock } from '@typebot.io/schemas'
import { WithVariableContent } from '@/features/graph/components/nodes/block/WithVariableContent'
import { defaultEmailInputOptions } from '@typebot.io/schemas/features/blocks/inputs/email/constants'
type Props = {
variableId?: string
placeholder: EmailInputBlock['options']['labels']['placeholder']
options: EmailInputBlock['options']
}
export const EmailInputNodeContent = ({ variableId, placeholder }: Props) =>
export const EmailInputNodeContent = ({
options: { variableId, labels } = {},
}: Props) =>
variableId ? (
<WithVariableContent variableId={variableId} />
) : (
<Text color={'gray.500'}>{placeholder}</Text>
<Text color={'gray.500'}>
{labels?.placeholder ?? defaultEmailInputOptions.labels.placeholder}
</Text>
)

View File

@@ -1,23 +1,20 @@
import { TextInput } from '@/components/inputs'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { FormLabel, Stack } from '@chakra-ui/react'
import {
EmailInputOptions,
Variable,
invalidEmailDefaultRetryMessage,
} from '@typebot.io/schemas'
import { EmailInputBlock, Variable } from '@typebot.io/schemas'
import { defaultEmailInputOptions } from '@typebot.io/schemas/features/blocks/inputs/email/constants'
import React from 'react'
type Props = {
options: EmailInputOptions
onOptionsChange: (options: EmailInputOptions) => void
options: EmailInputBlock['options']
onOptionsChange: (options: EmailInputBlock['options']) => void
}
export const EmailInputSettings = ({ options, onOptionsChange }: Props) => {
const handlePlaceholderChange = (placeholder: string) =>
onOptionsChange({ ...options, labels: { ...options.labels, placeholder } })
onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } })
const handleButtonLabelChange = (button: string) =>
onOptionsChange({ ...options, labels: { ...options.labels, button } })
onOptionsChange({ ...options, labels: { ...options?.labels, button } })
const handleVariableChange = (variable?: Variable) =>
onOptionsChange({ ...options, variableId: variable?.id })
const handleRetryMessageChange = (retryMessageContent: string) =>
@@ -27,18 +24,24 @@ export const EmailInputSettings = ({ options, onOptionsChange }: Props) => {
<Stack spacing={4}>
<TextInput
label="Placeholder:"
defaultValue={options.labels.placeholder}
defaultValue={
options?.labels?.placeholder ??
defaultEmailInputOptions.labels.placeholder
}
onChange={handlePlaceholderChange}
/>
<TextInput
label="Button label:"
defaultValue={options.labels.button}
defaultValue={
options?.labels?.button ?? defaultEmailInputOptions.labels.button
}
onChange={handleButtonLabelChange}
/>
<TextInput
label="Retry message:"
defaultValue={
options.retryMessageContent ?? invalidEmailDefaultRetryMessage
options?.retryMessageContent ??
defaultEmailInputOptions.retryMessageContent
}
onChange={handleRetryMessageChange}
/>
@@ -47,7 +50,7 @@ export const EmailInputSettings = ({ options, onOptionsChange }: Props) => {
Save answer in a variable:
</FormLabel>
<VariableSearchInput
initialVariableId={options.variableId}
initialVariableId={options?.variableId}
onSelectVariable={handleVariableChange}
/>
</Stack>

View File

@@ -1,12 +1,9 @@
import test, { expect } from '@playwright/test'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import {
defaultEmailInputOptions,
InputBlockType,
invalidEmailDefaultRetryMessage,
} from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { defaultEmailInputOptions } from '@typebot.io/schemas/features/blocks/inputs/email/constants'
test.describe('Email input block', () => {
test('options should work', async ({ page }) => {
@@ -16,7 +13,6 @@ test.describe('Email input block', () => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: InputBlockType.EMAIL,
options: defaultEmailInputOptions,
}),
},
])
@@ -39,7 +35,7 @@ test.describe('Email input block', () => {
await expect(page.locator('text=Your email...')).toBeVisible()
await page.getByLabel('Button label:').fill('Go')
await page.fill(
`input[value="${invalidEmailDefaultRetryMessage}"]`,
`input[value="${defaultEmailInputOptions.retryMessageContent}"]`,
'Try again bro'
)

View File

@@ -1,18 +1,16 @@
import { WithVariableContent } from '@/features/graph/components/nodes/block/WithVariableContent'
import { Text } from '@chakra-ui/react'
import { FileInputOptions } from '@typebot.io/schemas'
import { FileInputBlock } from '@typebot.io/schemas'
type Props = {
options: FileInputOptions
options: FileInputBlock['options']
}
export const FileInputContent = ({
options: { isMultipleAllowed, variableId },
}: Props) =>
variableId ? (
<WithVariableContent variableId={variableId} />
export const FileInputContent = ({ options }: Props) =>
options?.variableId ? (
<WithVariableContent variableId={options.variableId} />
) : (
<Text noOfLines={1} pr="6">
Collect {isMultipleAllowed ? 'files' : 'file'}
Collect {options?.isMultipleAllowed ? 'files' : 'file'}
</Text>
)

View File

@@ -1,22 +1,23 @@
import { FormLabel, Stack } from '@chakra-ui/react'
import { CodeEditor } from '@/components/inputs/CodeEditor'
import { FileInputOptions, Variable } from '@typebot.io/schemas'
import { FileInputBlock, Variable } from '@typebot.io/schemas'
import React from 'react'
import { TextInput } from '@/components/inputs'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { defaultFileInputOptions } from '@typebot.io/schemas/features/blocks/inputs/file/constants'
type Props = {
options: FileInputOptions
onOptionsChange: (options: FileInputOptions) => void
options: FileInputBlock['options']
onOptionsChange: (options: FileInputBlock['options']) => void
}
export const FileInputSettings = ({ options, onOptionsChange }: Props) => {
const handleButtonLabelChange = (button: string) =>
onOptionsChange({ ...options, labels: { ...options.labels, button } })
onOptionsChange({ ...options, labels: { ...options?.labels, button } })
const handlePlaceholderLabelChange = (placeholder: string) =>
onOptionsChange({ ...options, labels: { ...options.labels, placeholder } })
onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } })
const handleMultipleFilesChange = (isMultipleAllowed: boolean) =>
onOptionsChange({ ...options, isMultipleAllowed })
@@ -28,21 +29,24 @@ export const FileInputSettings = ({ options, onOptionsChange }: Props) => {
onOptionsChange({ ...options, isRequired })
const updateClearButtonLabel = (clear: string) =>
onOptionsChange({ ...options, labels: { ...options.labels, clear } })
onOptionsChange({ ...options, labels: { ...options?.labels, clear } })
const updateSkipButtonLabel = (skip: string) =>
onOptionsChange({ ...options, labels: { ...options.labels, skip } })
onOptionsChange({ ...options, labels: { ...options?.labels, skip } })
return (
<Stack spacing={4}>
<SwitchWithLabel
label="Required?"
initialValue={options.isRequired ?? true}
initialValue={options?.isRequired ?? defaultFileInputOptions.isRequired}
onCheckChange={handleRequiredChange}
/>
<SwitchWithLabel
label="Allow multiple files?"
initialValue={options.isMultipleAllowed}
initialValue={
options?.isMultipleAllowed ??
defaultFileInputOptions.isMultipleAllowed
}
onCheckChange={handleMultipleFilesChange}
/>
<Stack>
@@ -50,35 +54,44 @@ export const FileInputSettings = ({ options, onOptionsChange }: Props) => {
<CodeEditor
lang="html"
onChange={handlePlaceholderLabelChange}
defaultValue={options.labels.placeholder}
defaultValue={
options?.labels?.placeholder ??
defaultFileInputOptions.labels.placeholder
}
height={'100px'}
withVariableButton={false}
/>
</Stack>
<TextInput
label="Button label:"
defaultValue={options.labels.button}
defaultValue={
options?.labels?.button ?? defaultFileInputOptions.labels.button
}
onChange={handleButtonLabelChange}
withVariableButton={false}
/>
<TextInput
label="Clear button label:"
defaultValue={options.labels.clear ?? ''}
defaultValue={
options?.labels?.clear ?? defaultFileInputOptions.labels.clear
}
onChange={updateClearButtonLabel}
withVariableButton={false}
/>
<TextInput
label="Skip button label:"
defaultValue={options.labels.skip ?? ''}
defaultValue={
options?.labels?.skip ?? defaultFileInputOptions.labels.skip
}
onChange={updateSkipButtonLabel}
withVariableButton={false}
/>
<Stack>
<FormLabel mb="0" htmlFor="variable">
Save upload URL{options.isMultipleAllowed ? 's' : ''} in a variable:
Save upload URL{options?.isMultipleAllowed ? 's' : ''} in a variable:
</FormLabel>
<VariableSearchInput
initialVariableId={options.variableId}
initialVariableId={options?.variableId}
onSelectVariable={handleVariableChange}
/>
</Stack>

View File

@@ -1,10 +1,10 @@
import test, { expect } from '@playwright/test'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import { defaultFileInputOptions, InputBlockType } from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { freeWorkspaceId } from '@typebot.io/lib/playwright/databaseSetup'
import { getTestAsset } from '@/test/utils/playwright'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
test.describe.configure({ mode: 'parallel' })
@@ -15,7 +15,6 @@ test('options should work', async ({ page }) => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: InputBlockType.FILE,
options: defaultFileInputOptions,
}),
},
])
@@ -59,8 +58,8 @@ test.describe('Free workspace', () => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: InputBlockType.FILE,
options: defaultFileInputOptions,
}),
version: '6',
workspaceId: freeWorkspaceId,
},
])

View File

@@ -1,25 +1,29 @@
import { TextInput, NumberInput } from '@/components/inputs'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { FormLabel, Stack } from '@chakra-ui/react'
import { NumberInputOptions, Variable } from '@typebot.io/schemas'
import { NumberInputBlock, Variable } from '@typebot.io/schemas'
import { defaultNumberInputOptions } from '@typebot.io/schemas/features/blocks/inputs/number/constants'
import React from 'react'
type Props = {
options: NumberInputOptions
onOptionsChange: (options: NumberInputOptions) => void
options: NumberInputBlock['options']
onOptionsChange: (options: NumberInputBlock['options']) => void
}
export const NumberInputSettings = ({ options, onOptionsChange }: Props) => {
const handlePlaceholderChange = (placeholder: string) =>
onOptionsChange({ ...options, labels: { ...options.labels, placeholder } })
onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } })
const handleButtonLabelChange = (button: string) =>
onOptionsChange({ ...options, labels: { ...options.labels, button } })
const handleMinChange = (min?: NumberInputOptions['min']) =>
onOptionsChange({ ...options, min })
const handleMaxChange = (max?: NumberInputOptions['max']) =>
onOptionsChange({ ...options, max })
const handleStepChange = (step?: NumberInputOptions['step']) =>
onOptionsChange({ ...options, step })
onOptionsChange({ ...options, labels: { ...options?.labels, button } })
const handleMinChange = (
min?: NonNullable<NumberInputBlock['options']>['min']
) => onOptionsChange({ ...options, min })
const handleMaxChange = (
max?: NonNullable<NumberInputBlock['options']>['max']
) => onOptionsChange({ ...options, max })
const handleStepChange = (
step?: NonNullable<NumberInputBlock['options']>['step']
) => onOptionsChange({ ...options, step })
const handleVariableChange = (variable?: Variable) => {
onOptionsChange({ ...options, variableId: variable?.id })
}
@@ -28,27 +32,32 @@ export const NumberInputSettings = ({ options, onOptionsChange }: Props) => {
<Stack spacing={4}>
<TextInput
label="Placeholder:"
defaultValue={options.labels.placeholder}
defaultValue={
options?.labels?.placeholder ??
defaultNumberInputOptions.labels.placeholder
}
onChange={handlePlaceholderChange}
/>
<TextInput
label="Button label:"
defaultValue={options?.labels?.button ?? 'Send'}
defaultValue={
options?.labels?.button ?? defaultNumberInputOptions.labels.button
}
onChange={handleButtonLabelChange}
/>
<NumberInput
label="Min:"
defaultValue={options.min}
defaultValue={options?.min}
onValueChange={handleMinChange}
/>
<NumberInput
label="Max:"
defaultValue={options.max}
defaultValue={options?.max}
onValueChange={handleMaxChange}
/>
<NumberInput
label="Step:"
defaultValue={options.step}
defaultValue={options?.step}
onValueChange={handleStepChange}
/>
<Stack>
@@ -56,7 +65,7 @@ export const NumberInputSettings = ({ options, onOptionsChange }: Props) => {
Save answer in a variable:
</FormLabel>
<VariableSearchInput
initialVariableId={options.variableId}
initialVariableId={options?.variableId}
onSelectVariable={handleVariableChange}
/>
</Stack>

View File

@@ -2,15 +2,19 @@ import React from 'react'
import { Text } from '@chakra-ui/react'
import { NumberInputBlock } from '@typebot.io/schemas'
import { WithVariableContent } from '@/features/graph/components/nodes/block/WithVariableContent'
import { defaultNumberInputOptions } from '@typebot.io/schemas/features/blocks/inputs/number/constants'
type Props = {
variableId?: string
placeholder: NumberInputBlock['options']['labels']['placeholder']
options: NumberInputBlock['options']
}
export const NumberNodeContent = ({ variableId, placeholder }: Props) =>
export const NumberNodeContent = ({
options: { variableId, labels } = {},
}: Props) =>
variableId ? (
<WithVariableContent variableId={variableId} />
) : (
<Text color={'gray.500'}>{placeholder}</Text>
<Text color={'gray.500'}>
{labels?.placeholder ?? defaultNumberInputOptions.labels.placeholder}
</Text>
)

View File

@@ -1,8 +1,9 @@
import test, { expect } from '@playwright/test'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import { defaultNumberInputOptions, InputBlockType } from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { defaultNumberInputOptions } from '@typebot.io/schemas/features/blocks/inputs/number/constants'
test.describe('Number input block', () => {
test('options should work', async ({ page }) => {
@@ -12,7 +13,6 @@ test.describe('Number input block', () => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: InputBlockType.NUMBER,
options: defaultNumberInputOptions,
}),
},
])

View File

@@ -7,7 +7,7 @@ type Props = {
export const PaymentInputContent = ({ block }: Props) => {
if (
!block.options.amount ||
!block.options?.amount ||
!block.options.credentialsId ||
!block.options.currency
)

View File

@@ -11,11 +11,7 @@ import {
AccordionPanel,
} from '@chakra-ui/react'
import { DropdownList } from '@/components/DropdownList'
import {
PaymentAddress,
PaymentInputOptions,
PaymentProvider,
} from '@typebot.io/schemas'
import { PaymentAddress, PaymentInputBlock } from '@typebot.io/schemas'
import React, { ChangeEvent } from 'react'
import { currencies } from '../currencies'
import { StripeConfigModal } from './StripeConfigModal'
@@ -23,10 +19,14 @@ import { CredentialsDropdown } from '@/features/credentials/components/Credentia
import { TextInput } from '@/components/inputs'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { PaymentAddressSettings } from './PaymentAddressSettings'
import {
PaymentProvider,
defaultPaymentInputOptions,
} from '@typebot.io/schemas/features/blocks/inputs/payment/constants'
type Props = {
options: PaymentInputOptions
onOptionsChange: (options: PaymentInputOptions) => void
options: PaymentInputBlock['options']
onOptionsChange: (options: PaymentInputBlock['options']) => void
}
export const PaymentSettings = ({ options, onOptionsChange }: Props) => {
@@ -62,43 +62,43 @@ export const PaymentSettings = ({ options, onOptionsChange }: Props) => {
const updateName = (name: string) =>
onOptionsChange({
...options,
additionalInformation: { ...options.additionalInformation, name },
additionalInformation: { ...options?.additionalInformation, name },
})
const updateEmail = (email: string) =>
onOptionsChange({
...options,
additionalInformation: { ...options.additionalInformation, email },
additionalInformation: { ...options?.additionalInformation, email },
})
const updatePhoneNumber = (phoneNumber: string) =>
onOptionsChange({
...options,
additionalInformation: { ...options.additionalInformation, phoneNumber },
additionalInformation: { ...options?.additionalInformation, phoneNumber },
})
const updateButtonLabel = (button: string) =>
onOptionsChange({
...options,
labels: { ...options.labels, button },
labels: { ...options?.labels, button },
})
const updateSuccessLabel = (success: string) =>
onOptionsChange({
...options,
labels: { ...options.labels, success },
labels: { ...options?.labels, success },
})
const updateDescription = (description: string) =>
onOptionsChange({
...options,
additionalInformation: { ...options.additionalInformation, description },
additionalInformation: { ...options?.additionalInformation, description },
})
const updateAddress = (address: PaymentAddress) =>
onOptionsChange({
...options,
additionalInformation: { ...options.additionalInformation, address },
additionalInformation: { ...options?.additionalInformation, address },
})
return (
@@ -108,7 +108,7 @@ export const PaymentSettings = ({ options, onOptionsChange }: Props) => {
<DropdownList
onItemSelect={updateProvider}
items={Object.values(PaymentProvider)}
currentItem={options.provider}
currentItem={options?.provider ?? defaultPaymentInputOptions.provider}
/>
</Stack>
<Stack>
@@ -117,7 +117,7 @@ export const PaymentSettings = ({ options, onOptionsChange }: Props) => {
<CredentialsDropdown
type="stripe"
workspaceId={workspace.id}
currentCredentialsId={options.credentialsId}
currentCredentialsId={options?.credentialsId}
onCredentialsSelect={updateCredentials}
onCreateNewClick={onOpen}
credentialsName="Stripe account"
@@ -128,14 +128,14 @@ export const PaymentSettings = ({ options, onOptionsChange }: Props) => {
<TextInput
label="Price amount:"
onChange={updateAmount}
defaultValue={options.amount ?? ''}
defaultValue={options?.amount}
placeholder="30.00"
/>
<Stack>
<Text>Currency:</Text>
<Select
placeholder="Select option"
value={options.currency}
value={options?.currency ?? defaultPaymentInputOptions.currency}
onChange={updateCurrency}
>
{currencies.map((currency) => (
@@ -149,14 +149,16 @@ export const PaymentSettings = ({ options, onOptionsChange }: Props) => {
<TextInput
label="Button label:"
onChange={updateButtonLabel}
defaultValue={options.labels.button}
placeholder="Pay"
defaultValue={
options?.labels?.button ?? defaultPaymentInputOptions.labels.button
}
/>
<TextInput
label="Success message:"
onChange={updateSuccessLabel}
defaultValue={options.labels.success ?? 'Success'}
placeholder="Success"
defaultValue={
options?.labels?.success ?? defaultPaymentInputOptions.labels.success
}
/>
<Accordion allowToggle>
<AccordionItem>
@@ -167,30 +169,30 @@ export const PaymentSettings = ({ options, onOptionsChange }: Props) => {
<AccordionPanel py={4} as={Stack} spacing="6">
<TextInput
label="Description:"
defaultValue={options.additionalInformation?.description ?? ''}
defaultValue={options?.additionalInformation?.description}
onChange={updateDescription}
placeholder="A digital product"
/>
<TextInput
label="Name:"
defaultValue={options.additionalInformation?.name ?? ''}
defaultValue={options?.additionalInformation?.name}
onChange={updateName}
placeholder="John Smith"
/>
<TextInput
label="Email:"
defaultValue={options.additionalInformation?.email ?? ''}
defaultValue={options?.additionalInformation?.email}
onChange={updateEmail}
placeholder="john@gmail.com"
/>
<TextInput
label="Phone number:"
defaultValue={options.additionalInformation?.phoneNumber ?? ''}
defaultValue={options?.additionalInformation?.phoneNumber}
onChange={updatePhoneNumber}
placeholder="+33XXXXXXXXX"
/>
<PaymentAddressSettings
address={options.additionalInformation?.address}
address={options?.additionalInformation?.address}
onAddressChange={updateAddress}
/>
</AccordionPanel>

View File

@@ -1,10 +1,10 @@
import test, { expect } from '@playwright/test'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import { defaultPaymentInputOptions, InputBlockType } from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { stripePaymentForm } from '@/test/utils/selectorUtils'
import { env } from '@typebot.io/env'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
test.describe('Payment input block', () => {
test('Can configure Stripe account', async ({ page }) => {
@@ -14,7 +14,6 @@ test.describe('Payment input block', () => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: InputBlockType.PAYMENT,
options: defaultPaymentInputOptions,
}),
},
])

View File

@@ -1,20 +1,21 @@
import { TextInput } from '@/components/inputs'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { FormLabel, Stack } from '@chakra-ui/react'
import { PhoneNumberInputOptions, Variable } from '@typebot.io/schemas'
import { PhoneNumberInputBlock, Variable } from '@typebot.io/schemas'
import React from 'react'
import { CountryCodeSelect } from './CountryCodeSelect'
import { defaultPhoneInputOptions } from '@typebot.io/schemas/features/blocks/inputs/phone/constants'
type Props = {
options: PhoneNumberInputOptions
onOptionsChange: (options: PhoneNumberInputOptions) => void
options: PhoneNumberInputBlock['options']
onOptionsChange: (options: PhoneNumberInputBlock['options']) => void
}
export const PhoneInputSettings = ({ options, onOptionsChange }: Props) => {
const handlePlaceholderChange = (placeholder: string) =>
onOptionsChange({ ...options, labels: { ...options.labels, placeholder } })
onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } })
const handleButtonLabelChange = (button: string) =>
onOptionsChange({ ...options, labels: { ...options.labels, button } })
onOptionsChange({ ...options, labels: { ...options?.labels, button } })
const handleVariableChange = (variable?: Variable) =>
onOptionsChange({ ...options, variableId: variable?.id })
const handleRetryMessageChange = (retryMessageContent: string) =>
@@ -26,12 +27,17 @@ export const PhoneInputSettings = ({ options, onOptionsChange }: Props) => {
<Stack spacing={4}>
<TextInput
label="Placeholder:"
defaultValue={options.labels.placeholder}
defaultValue={
options?.labels?.placeholder ??
defaultPhoneInputOptions.labels.placeholder
}
onChange={handlePlaceholderChange}
/>
<TextInput
label="Button label:"
defaultValue={options.labels.button}
defaultValue={
options?.labels?.button ?? defaultPhoneInputOptions.labels.button
}
onChange={handleButtonLabelChange}
/>
<Stack>
@@ -40,12 +46,15 @@ export const PhoneInputSettings = ({ options, onOptionsChange }: Props) => {
</FormLabel>
<CountryCodeSelect
onSelect={handleDefaultCountryChange}
countryCode={options.defaultCountryCode}
countryCode={options?.defaultCountryCode}
/>
</Stack>
<TextInput
label="Retry message:"
defaultValue={options.retryMessageContent}
defaultValue={
options?.retryMessageContent ??
defaultPhoneInputOptions.retryMessageContent
}
onChange={handleRetryMessageChange}
/>
<Stack>
@@ -53,7 +62,7 @@ export const PhoneInputSettings = ({ options, onOptionsChange }: Props) => {
Save answer in a variable:
</FormLabel>
<VariableSearchInput
initialVariableId={options.variableId}
initialVariableId={options?.variableId}
onSelectVariable={handleVariableChange}
/>
</Stack>

View File

@@ -1,16 +1,20 @@
import React from 'react'
import { Text } from '@chakra-ui/react'
import { PhoneNumberInputOptions } from '@typebot.io/schemas'
import { WithVariableContent } from '@/features/graph/components/nodes/block/WithVariableContent'
import { PhoneNumberInputBlock } from '@typebot.io/schemas'
import { defaultPhoneInputOptions } from '@typebot.io/schemas/features/blocks/inputs/phone/constants'
type Props = {
variableId?: string
placeholder: PhoneNumberInputOptions['labels']['placeholder']
options: PhoneNumberInputBlock['options']
}
export const PhoneNodeContent = ({ variableId, placeholder }: Props) =>
export const PhoneNodeContent = ({
options: { variableId, labels } = {},
}: Props) =>
variableId ? (
<WithVariableContent variableId={variableId} />
) : (
<Text color={'gray.500'}>{placeholder}</Text>
<Text color={'gray.500'}>
{labels?.placeholder ?? defaultPhoneInputOptions.labels.placeholder}
</Text>
)

View File

@@ -1,8 +1,9 @@
import test, { expect } from '@playwright/test'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import { defaultPhoneInputOptions, InputBlockType } from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { defaultPhoneInputOptions } from '@typebot.io/schemas/features/blocks/inputs/phone/constants'
test.describe('Phone input block', () => {
test('options should work', async ({ page }) => {
@@ -12,7 +13,6 @@ test.describe('Phone input block', () => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: InputBlockType.PHONE,
options: defaultPhoneInputOptions,
}),
},
])

View File

@@ -14,7 +14,7 @@ import {
} from '@chakra-ui/react'
import { ImageIcon, PlusIcon } from '@/components/icons'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { ItemIndices, ItemType } from '@typebot.io/schemas'
import { ItemIndices } from '@typebot.io/schemas'
import React, { useRef } from 'react'
import { PictureChoiceItem } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
import { useGraph } from '@/features/graph/providers/GraphProvider'
@@ -40,7 +40,7 @@ export const PictureChoiceItemNode = ({
const handlePlusClick = (e: React.MouseEvent) => {
e.stopPropagation()
const itemIndex = indices.itemIndex + 1
createItem({ type: ItemType.PICTURE_CHOICE }, { ...indices, itemIndex })
createItem({}, { ...indices, itemIndex })
}
const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation()

View File

@@ -13,7 +13,8 @@ import {
import { ImageUploadContent } from '@/components/ImageUploadContent'
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
import { ConditionForm } from '@/features/blocks/logic/condition/components/ConditionForm'
import { Condition, LogicalOperator } from '@typebot.io/schemas'
import { Condition } from '@typebot.io/schemas'
import { LogicalOperator } from '@typebot.io/schemas/features/blocks/logic/condition/constants'
type Props = {
workspaceId: string

View File

@@ -15,12 +15,12 @@ export const PictureChoiceNode = ({ block, indices }: Props) => {
const { typebot } = useTypebot()
const dynamicVariableName = typebot?.variables.find(
(variable) =>
variable.id === block.options.dynamicItems?.pictureSrcsVariableId
variable.id === block.options?.dynamicItems?.pictureSrcsVariableId
)?.name
return (
<Stack w="full">
{block.options.dynamicItems?.isEnabled && dynamicVariableName ? (
{block.options?.dynamicItems?.isEnabled && dynamicVariableName ? (
<Wrap spacing={1}>
<Text>Display</Text>
<Tag bg="orange.400" color="white">
@@ -31,7 +31,7 @@ export const PictureChoiceNode = ({ block, indices }: Props) => {
) : (
<ItemNodesList block={block} indices={indices} />
)}
{block.options.variableId ? (
{block.options?.variableId ? (
<SetVariableLabel
variableId={block.options.variableId}
variables={typebot?.variables}

View File

@@ -3,11 +3,9 @@ import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { FormLabel, Stack } from '@chakra-ui/react'
import { Variable } from '@typebot.io/schemas'
import React from 'react'
import {
PictureChoiceBlock,
defaultPictureChoiceOptions,
} from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
import { PictureChoiceBlock } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice/constants'
type Props = {
options?: PictureChoiceBlock['options']
@@ -16,52 +14,48 @@ type Props = {
export const PictureChoiceSettings = ({ options, onOptionsChange }: Props) => {
const updateIsMultiple = (isMultipleChoice: boolean) =>
options && onOptionsChange({ ...options, isMultipleChoice })
onOptionsChange({ ...options, isMultipleChoice })
const updateButtonLabel = (buttonLabel: string) =>
options && onOptionsChange({ ...options, buttonLabel })
onOptionsChange({ ...options, buttonLabel })
const updateSaveVariable = (variable?: Variable) =>
options && onOptionsChange({ ...options, variableId: variable?.id })
onOptionsChange({ ...options, variableId: variable?.id })
const updateSearchInputPlaceholder = (searchInputPlaceholder: string) =>
options && onOptionsChange({ ...options, searchInputPlaceholder })
onOptionsChange({ ...options, searchInputPlaceholder })
const updateIsSearchable = (isSearchable: boolean) =>
options && onOptionsChange({ ...options, isSearchable })
onOptionsChange({ ...options, isSearchable })
const updateIsDynamicItemsEnabled = (isEnabled: boolean) =>
options &&
onOptionsChange({
...options,
dynamicItems: {
...options.dynamicItems,
...options?.dynamicItems,
isEnabled,
},
})
const updateDynamicItemsPictureSrcsVariable = (variable?: Variable) =>
options &&
onOptionsChange({
...options,
dynamicItems: {
...options.dynamicItems,
...options?.dynamicItems,
pictureSrcsVariableId: variable?.id,
},
})
const updateDynamicItemsTitlesVariable = (variable?: Variable) =>
options &&
onOptionsChange({
...options,
dynamicItems: {
...options.dynamicItems,
...options?.dynamicItems,
titlesVariableId: variable?.id,
},
})
const updateDynamicItemsDescriptionsVariable = (variable?: Variable) =>
options &&
onOptionsChange({
...options,
dynamicItems: {
...options.dynamicItems,
...options?.dynamicItems,
descriptionsVariableId: variable?.id,
},
})
@@ -70,7 +64,9 @@ export const PictureChoiceSettings = ({ options, onOptionsChange }: Props) => {
<Stack spacing={4}>
<SwitchWithRelatedSettings
label="Is searchable?"
initialValue={options?.isSearchable ?? false}
initialValue={
options?.isSearchable ?? defaultPictureChoiceOptions.isSearchable
}
onCheckChange={updateIsSearchable}
>
<TextInput
@@ -84,19 +80,27 @@ export const PictureChoiceSettings = ({ options, onOptionsChange }: Props) => {
</SwitchWithRelatedSettings>
<SwitchWithRelatedSettings
label="Multiple choice?"
initialValue={options?.isMultipleChoice ?? false}
initialValue={
options?.isMultipleChoice ??
defaultPictureChoiceOptions.isMultipleChoice
}
onCheckChange={updateIsMultiple}
>
<TextInput
label="Submit button label:"
defaultValue={options?.buttonLabel ?? 'Send'}
defaultValue={
options?.buttonLabel ?? defaultPictureChoiceOptions.buttonLabel
}
onChange={updateButtonLabel}
/>
</SwitchWithRelatedSettings>
<SwitchWithRelatedSettings
label="Dynamic items?"
initialValue={options?.dynamicItems?.isEnabled ?? false}
initialValue={
options?.dynamicItems?.isEnabled ??
defaultPictureChoiceOptions.dynamicItems.isEnabled
}
onCheckChange={updateIsDynamicItemsEnabled}
>
<Stack>

View File

@@ -1,9 +1,8 @@
import test, { expect } from '@playwright/test'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import { InputBlockType, ItemType } from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { defaultPictureChoiceOptions } from '@typebot.io/schemas/features/blocks/inputs/pictureChoice'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
const firstImageSrc =
'https://images.unsplash.com/flagged/photo-1575517111839-3a3843ee7f5d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2940&q=80'
@@ -25,10 +24,8 @@ test.describe.parallel('Picture choice input block', () => {
items: [
{
id: 'choice1',
type: ItemType.PICTURE_CHOICE,
},
],
options: { ...defaultPictureChoiceOptions },
}),
},
])

View File

@@ -1,6 +1,7 @@
import { WithVariableContent } from '@/features/graph/components/nodes/block/WithVariableContent'
import { Text } from '@chakra-ui/react'
import { RatingInputBlock } from '@typebot.io/schemas'
import { defaultRatingInputOptions } from '@typebot.io/schemas/features/blocks/inputs/rating/constants'
type Props = {
variableId?: string
@@ -12,7 +13,7 @@ export const RatingInputContent = ({ variableId, block }: Props) =>
<WithVariableContent variableId={variableId} />
) : (
<Text noOfLines={1} pr="6">
Rate from {block.options.buttonType === 'Icons' ? 1 : 0} to{' '}
{block.options.length}
Rate from {block.options?.buttonType === 'Icons' ? 1 : 0} to{' '}
{block.options?.length ?? defaultRatingInputOptions.length}
</Text>
)

View File

@@ -1,14 +1,15 @@
import { FormLabel, Stack } from '@chakra-ui/react'
import { DropdownList } from '@/components/DropdownList'
import { RatingInputOptions, Variable } from '@typebot.io/schemas'
import { RatingInputBlock, Variable } from '@typebot.io/schemas'
import React from 'react'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { TextInput } from '@/components/inputs'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { defaultRatingInputOptions } from '@typebot.io/schemas/features/blocks/inputs/rating/constants'
type Props = {
options: RatingInputOptions
onOptionsChange: (options: RatingInputOptions) => void
options: RatingInputBlock['options']
onOptionsChange: (options: RatingInputBlock['options']) => void
}
export const RatingInputSettings = ({ options, onOptionsChange }: Props) => {
@@ -21,20 +22,20 @@ export const RatingInputSettings = ({ options, onOptionsChange }: Props) => {
const handleCustomIconCheck = (isEnabled: boolean) =>
onOptionsChange({
...options,
customIcon: { ...options.customIcon, isEnabled },
customIcon: { ...options?.customIcon, isEnabled },
})
const handleIconSvgChange = (svg: string) =>
onOptionsChange({ ...options, customIcon: { ...options.customIcon, svg } })
onOptionsChange({ ...options, customIcon: { ...options?.customIcon, svg } })
const handleLeftLabelChange = (left: string) =>
onOptionsChange({ ...options, labels: { ...options.labels, left } })
onOptionsChange({ ...options, labels: { ...options?.labels, left } })
const handleRightLabelChange = (right: string) =>
onOptionsChange({ ...options, labels: { ...options.labels, right } })
onOptionsChange({ ...options, labels: { ...options?.labels, right } })
const handleButtonLabelChange = (button: string) =>
onOptionsChange({ ...options, labels: { ...options.labels, button } })
onOptionsChange({ ...options, labels: { ...options?.labels, button } })
const handleVariableChange = (variable?: Variable) =>
onOptionsChange({ ...options, variableId: variable?.id })
@@ -42,6 +43,11 @@ export const RatingInputSettings = ({ options, onOptionsChange }: Props) => {
const handleOneClickSubmitChange = (isOneClickSubmitEnabled: boolean) =>
onOptionsChange({ ...options, isOneClickSubmitEnabled })
const length = options?.length ?? defaultRatingInputOptions.length
const isOneClickSubmitEnabled =
options?.isOneClickSubmitEnabled ??
defaultRatingInputOptions.isOneClickSubmitEnabled
return (
<Stack spacing={4}>
<Stack>
@@ -51,7 +57,7 @@ export const RatingInputSettings = ({ options, onOptionsChange }: Props) => {
<DropdownList
onItemSelect={handleLengthChange}
items={[3, 4, 5, 6, 7, 8, 9, 10]}
currentItem={options.length}
currentItem={length}
/>
</Stack>
@@ -62,18 +68,23 @@ export const RatingInputSettings = ({ options, onOptionsChange }: Props) => {
<DropdownList
onItemSelect={handleTypeChange}
items={['Icons', 'Numbers'] as const}
currentItem={options.buttonType}
currentItem={
options?.buttonType ?? defaultRatingInputOptions.buttonType
}
/>
</Stack>
{options.buttonType === 'Icons' && (
{options?.buttonType === 'Icons' && (
<SwitchWithLabel
label="Custom icon?"
initialValue={options.customIcon.isEnabled}
initialValue={
options?.customIcon?.isEnabled ??
defaultRatingInputOptions.customIcon.isEnabled
}
onCheckChange={handleCustomIconCheck}
/>
)}
{options.buttonType === 'Icons' && options.customIcon.isEnabled && (
{options?.buttonType === 'Icons' && options.customIcon?.isEnabled && (
<TextInput
label="Icon SVG:"
defaultValue={options.customIcon.svg}
@@ -82,27 +93,29 @@ export const RatingInputSettings = ({ options, onOptionsChange }: Props) => {
/>
)}
<TextInput
label={`${options.buttonType === 'Icons' ? '1' : '0'} label:`}
defaultValue={options.labels.left}
label={`${options?.buttonType === 'Icons' ? '1' : '0'} label:`}
defaultValue={options?.labels?.left}
onChange={handleLeftLabelChange}
placeholder="Not likely at all"
/>
<TextInput
label={`${options.length} label:`}
defaultValue={options.labels.right}
label={`${length} label:`}
defaultValue={options?.labels?.right}
onChange={handleRightLabelChange}
placeholder="Extremely likely"
/>
<SwitchWithLabel
label="One click submit"
moreInfoContent='If enabled, the answer will be submitted as soon as the user clicks on a rating instead of showing the "Send" button.'
initialValue={options.isOneClickSubmitEnabled ?? false}
initialValue={isOneClickSubmitEnabled}
onCheckChange={handleOneClickSubmitChange}
/>
{!options.isOneClickSubmitEnabled && (
{!isOneClickSubmitEnabled && (
<TextInput
label="Button label:"
defaultValue={options.labels.button}
defaultValue={
options?.labels?.button ?? defaultRatingInputOptions.labels.button
}
onChange={handleButtonLabelChange}
/>
)}
@@ -111,7 +124,7 @@ export const RatingInputSettings = ({ options, onOptionsChange }: Props) => {
Save answer in a variable:
</FormLabel>
<VariableSearchInput
initialVariableId={options.variableId}
initialVariableId={options?.variableId}
onSelectVariable={handleVariableChange}
/>
</Stack>

View File

@@ -1,8 +1,8 @@
import test, { expect } from '@playwright/test'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import { defaultRatingInputOptions, InputBlockType } from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
const boxSvg = `<svg
xmlns="http://www.w3.org/2000/svg"
@@ -24,7 +24,6 @@ test('options should work', async ({ page }) => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: InputBlockType.RATING,
options: defaultRatingInputOptions,
}),
},
])

View File

@@ -1,29 +1,25 @@
import React from 'react'
import { Text } from '@chakra-ui/react'
import { TextInputOptions } from '@typebot.io/schemas'
import { WithVariableContent } from '@/features/graph/components/nodes/block/WithVariableContent'
import { TextInputBlock } from '@typebot.io/schemas'
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
type Props = {
placeholder: TextInputOptions['labels']['placeholder']
isLong: TextInputOptions['isLong']
variableId?: string
options: TextInputBlock['options']
}
export const TextInputNodeContent = ({
placeholder,
isLong,
variableId,
}: Props) => {
if (variableId)
export const TextInputNodeContent = ({ options }: Props) => {
if (options?.variableId)
return (
<WithVariableContent
variableId={variableId}
h={isLong ? '100px' : 'auto'}
variableId={options?.variableId}
h={options.isLong ? '100px' : 'auto'}
/>
)
return (
<Text color={'gray.500'} h={isLong ? '100px' : 'auto'}>
{placeholder}
<Text color={'gray.500'} h={options?.isLong ? '100px' : 'auto'}>
{options?.labels?.placeholder ??
defaultTextInputOptions.labels.placeholder}
</Text>
)
}

View File

@@ -2,19 +2,20 @@ import { TextInput } from '@/components/inputs'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { FormLabel, Stack } from '@chakra-ui/react'
import { TextInputOptions, Variable } from '@typebot.io/schemas'
import { TextInputBlock, Variable } from '@typebot.io/schemas'
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
import React from 'react'
type Props = {
options: TextInputOptions
onOptionsChange: (options: TextInputOptions) => void
options: TextInputBlock['options']
onOptionsChange: (options: TextInputBlock['options']) => void
}
export const TextInputSettings = ({ options, onOptionsChange }: Props) => {
const handlePlaceholderChange = (placeholder: string) =>
onOptionsChange({ ...options, labels: { ...options.labels, placeholder } })
onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } })
const handleButtonLabelChange = (button: string) =>
onOptionsChange({ ...options, labels: { ...options.labels, button } })
onOptionsChange({ ...options, labels: { ...options?.labels, button } })
const handleLongChange = (isLong: boolean) =>
onOptionsChange({ ...options, isLong })
const handleVariableChange = (variable?: Variable) =>
@@ -24,17 +25,22 @@ export const TextInputSettings = ({ options, onOptionsChange }: Props) => {
<Stack spacing={4}>
<SwitchWithLabel
label="Long text?"
initialValue={options?.isLong ?? false}
initialValue={options?.isLong ?? defaultTextInputOptions.isLong}
onCheckChange={handleLongChange}
/>
<TextInput
label="Placeholder:"
defaultValue={options.labels.placeholder}
defaultValue={
options?.labels?.placeholder ??
defaultTextInputOptions.labels.placeholder
}
onChange={handlePlaceholderChange}
/>
<TextInput
label="Button label:"
defaultValue={options.labels.button}
defaultValue={
options?.labels?.button ?? defaultTextInputOptions.labels.button
}
onChange={handleButtonLabelChange}
/>
<Stack>
@@ -42,7 +48,7 @@ export const TextInputSettings = ({ options, onOptionsChange }: Props) => {
Save answer in a variable:
</FormLabel>
<VariableSearchInput
initialVariableId={options.variableId}
initialVariableId={options?.variableId}
onSelectVariable={handleVariableChange}
/>
</Stack>

View File

@@ -1,8 +1,9 @@
import test, { expect } from '@playwright/test'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import { defaultTextInputOptions, InputBlockType } from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
test.describe.parallel('Text input block', () => {
test('options should work', async ({ page }) => {
@@ -12,7 +13,6 @@ test.describe.parallel('Text input block', () => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: InputBlockType.TEXT,
options: defaultTextInputOptions,
}),
},
])

View File

@@ -1,19 +1,20 @@
import { TextInput } from '@/components/inputs'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { FormLabel, Stack } from '@chakra-ui/react'
import { UrlInputOptions, Variable } from '@typebot.io/schemas'
import { UrlInputBlock, Variable } from '@typebot.io/schemas'
import { defaultUrlInputOptions } from '@typebot.io/schemas/features/blocks/inputs/url/constants'
import React from 'react'
type Props = {
options: UrlInputOptions
onOptionsChange: (options: UrlInputOptions) => void
options: UrlInputBlock['options']
onOptionsChange: (options: UrlInputBlock['options']) => void
}
export const UrlInputSettings = ({ options, onOptionsChange }: Props) => {
const handlePlaceholderChange = (placeholder: string) =>
onOptionsChange({ ...options, labels: { ...options.labels, placeholder } })
onOptionsChange({ ...options, labels: { ...options?.labels, placeholder } })
const handleButtonLabelChange = (button: string) =>
onOptionsChange({ ...options, labels: { ...options.labels, button } })
onOptionsChange({ ...options, labels: { ...options?.labels, button } })
const handleVariableChange = (variable?: Variable) =>
onOptionsChange({ ...options, variableId: variable?.id })
const handleRetryMessageChange = (retryMessageContent: string) =>
@@ -23,17 +24,25 @@ export const UrlInputSettings = ({ options, onOptionsChange }: Props) => {
<Stack spacing={4}>
<TextInput
label="Placeholder:"
defaultValue={options.labels.placeholder}
defaultValue={
options?.labels?.placeholder ??
defaultUrlInputOptions.labels.placeholder
}
onChange={handlePlaceholderChange}
/>
<TextInput
label="Button label:"
defaultValue={options.labels.button}
defaultValue={
options?.labels?.button ?? defaultUrlInputOptions.labels.button
}
onChange={handleButtonLabelChange}
/>
<TextInput
label="Retry message:"
defaultValue={options.retryMessageContent}
defaultValue={
options?.retryMessageContent ??
defaultUrlInputOptions.retryMessageContent
}
onChange={handleRetryMessageChange}
/>
<Stack>
@@ -41,7 +50,7 @@ export const UrlInputSettings = ({ options, onOptionsChange }: Props) => {
Save answer in a variable:
</FormLabel>
<VariableSearchInput
initialVariableId={options.variableId}
initialVariableId={options?.variableId}
onSelectVariable={handleVariableChange}
/>
</Stack>

View File

@@ -1,18 +1,19 @@
import React from 'react'
import { Text } from '@chakra-ui/react'
import { UrlInputOptions } from '@typebot.io/schemas'
import { WithVariableContent } from '@/features/graph/components/nodes/block/WithVariableContent'
import { UrlInputBlock } from '@typebot.io/schemas'
import { defaultUrlInputOptions } from '@typebot.io/schemas/features/blocks/inputs/url/constants'
type Props = {
variableId?: string
placeholder: UrlInputOptions['labels']['placeholder']
options: UrlInputBlock['options']
}
export const UrlNodeContent = ({ placeholder, variableId }: Props) =>
variableId ? (
<WithVariableContent variableId={variableId} />
export const UrlNodeContent = ({ options }: Props) =>
options?.variableId ? (
<WithVariableContent variableId={options.variableId} />
) : (
<Text color={'gray.500'} w="90%">
{placeholder}
{options?.labels?.placeholder ??
defaultUrlInputOptions.labels.placeholder}
</Text>
)

View File

@@ -1,8 +1,9 @@
import test, { expect } from '@playwright/test'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import { defaultUrlInputOptions, InputBlockType } from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { defaultUrlInputOptions } from '@typebot.io/schemas/features/blocks/inputs/url/constants'
test.describe('Url input block', () => {
test('options should work', async ({ page }) => {
@@ -12,7 +13,6 @@ test.describe('Url input block', () => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: InputBlockType.URL,
options: defaultUrlInputOptions,
}),
},
])

View File

@@ -2,10 +2,8 @@ import test, { expect } from '@playwright/test'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import { createId } from '@paralleldrive/cuid2'
import {
defaultChatwootOptions,
IntegrationBlockType,
} from '@typebot.io/schemas'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
import { defaultChatwootOptions } from '@typebot.io/schemas/features/blocks/integrations/chatwoot/constants'
const typebotId = createId()
@@ -18,7 +16,6 @@ test.describe('Chatwoot block', () => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: IntegrationBlockType.CHATWOOT,
options: defaultChatwootOptions,
}),
},
])

View File

@@ -6,9 +6,9 @@ type Props = {
}
export const ChatwootNodeBody = ({ block }: Props) =>
block.options.task === 'Close widget' ? (
block.options?.task === 'Close widget' ? (
<Text>Close Chatwoot</Text>
) : block.options.websiteToken.length === 0 ? (
) : (block.options?.websiteToken?.length ?? 0) === 0 ? (
<Text color="gray.500">Configure...</Text>
) : (
<Text>Open Chatwoot</Text>

View File

@@ -8,12 +8,16 @@ import {
AccordionPanel,
Stack,
} from '@chakra-ui/react'
import { ChatwootOptions, chatwootTasks } from '@typebot.io/schemas'
import { ChatwootBlock } from '@typebot.io/schemas'
import {
chatwootTasks,
defaultChatwootOptions,
} from '@typebot.io/schemas/features/blocks/integrations/chatwoot/constants'
import React from 'react'
type Props = {
options: ChatwootOptions
onOptionsChange: (options: ChatwootOptions) => void
options: ChatwootBlock['options']
onOptionsChange: (options: ChatwootBlock['options']) => void
}
export const ChatwootSettings = ({ options, onOptionsChange }: Props) => {
@@ -21,96 +25,97 @@ export const ChatwootSettings = ({ options, onOptionsChange }: Props) => {
onOptionsChange({ ...options, task })
}
const task = options?.task ?? defaultChatwootOptions.task
return (
<Stack spacing={4}>
<DropdownList
currentItem={options.task ?? 'Show widget'}
currentItem={options?.task ?? defaultChatwootOptions.task}
onItemSelect={updateTask}
items={chatwootTasks}
/>
{!options.task ||
(options.task === 'Show widget' && (
<>
<TextInput
isRequired
label="Base URL"
defaultValue={options.baseUrl}
onChange={(baseUrl: string) => {
onOptionsChange({ ...options, baseUrl })
}}
withVariableButton={false}
/>
<TextInput
isRequired
label="Website token"
defaultValue={options.websiteToken}
onChange={(websiteToken) =>
onOptionsChange({ ...options, websiteToken })
}
moreInfoTooltip="Can be found in Chatwoot under Settings > Inboxes > Settings > Configuration, in the code snippet."
/>
<Accordion allowMultiple>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Set user details
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="4">
<TextInput
label="ID"
defaultValue={options.user?.id}
onChange={(id: string) => {
onOptionsChange({
...options,
user: { ...options.user, id },
})
}}
/>
<TextInput
label="Name"
defaultValue={options.user?.name}
onChange={(name: string) => {
onOptionsChange({
...options,
user: { ...options.user, name },
})
}}
/>
<TextInput
label="Email"
defaultValue={options.user?.email}
onChange={(email: string) => {
onOptionsChange({
...options,
user: { ...options.user, email },
})
}}
/>
<TextInput
label="Avatar URL"
defaultValue={options.user?.avatarUrl}
onChange={(avatarUrl: string) => {
onOptionsChange({
...options,
user: { ...options.user, avatarUrl },
})
}}
/>
<TextInput
label="Phone number"
defaultValue={options.user?.phoneNumber}
onChange={(phoneNumber: string) => {
onOptionsChange({
...options,
user: { ...options.user, phoneNumber },
})
}}
/>
</AccordionPanel>
</AccordionItem>
</Accordion>
</>
))}
{task === 'Show widget' && (
<>
<TextInput
isRequired
label="Base URL"
defaultValue={options?.baseUrl ?? defaultChatwootOptions.baseUrl}
onChange={(baseUrl: string) => {
onOptionsChange({ ...options, baseUrl })
}}
withVariableButton={false}
/>
<TextInput
isRequired
label="Website token"
defaultValue={options?.websiteToken}
onChange={(websiteToken) =>
onOptionsChange({ ...options, websiteToken })
}
moreInfoTooltip="Can be found in Chatwoot under Settings > Inboxes > Settings > Configuration, in the code snippet."
/>
<Accordion allowMultiple>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Set user details
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="4">
<TextInput
label="ID"
defaultValue={options?.user?.id}
onChange={(id: string) => {
onOptionsChange({
...options,
user: { ...options?.user, id },
})
}}
/>
<TextInput
label="Name"
defaultValue={options?.user?.name}
onChange={(name: string) => {
onOptionsChange({
...options,
user: { ...options?.user, name },
})
}}
/>
<TextInput
label="Email"
defaultValue={options?.user?.email}
onChange={(email: string) => {
onOptionsChange({
...options,
user: { ...options?.user, email },
})
}}
/>
<TextInput
label="Avatar URL"
defaultValue={options?.user?.avatarUrl}
onChange={(avatarUrl: string) => {
onOptionsChange({
...options,
user: { ...options?.user, avatarUrl },
})
}}
/>
<TextInput
label="Phone number"
defaultValue={options?.user?.phoneNumber}
onChange={(phoneNumber: string) => {
onOptionsChange({
...options,
user: { ...options?.user, phoneNumber },
})
}}
/>
</AccordionPanel>
</AccordionItem>
</Accordion>
</>
)}
</Stack>
)
}

View File

@@ -1,9 +1,9 @@
import React from 'react'
import { Text } from '@chakra-ui/react'
import { GoogleAnalyticsOptions } from '@typebot.io/schemas'
import { GoogleAnalyticsBlock } from '@typebot.io/schemas'
type Props = {
action: GoogleAnalyticsOptions['action']
action: NonNullable<GoogleAnalyticsBlock['options']>['action']
}
export const GoogleAnalyticsNodeBody = ({ action }: Props) => (

View File

@@ -8,12 +8,12 @@ import {
Box,
Stack,
} from '@chakra-ui/react'
import { GoogleAnalyticsOptions } from '@typebot.io/schemas'
import { GoogleAnalyticsBlock } from '@typebot.io/schemas'
import React from 'react'
type Props = {
options?: GoogleAnalyticsOptions
onOptionsChange: (options: GoogleAnalyticsOptions) => void
options?: GoogleAnalyticsBlock['options']
onOptionsChange: (options: GoogleAnalyticsBlock['options']) => void
}
export const GoogleAnalyticsSettings = ({
@@ -48,13 +48,13 @@ export const GoogleAnalyticsSettings = ({
<TextInput
label="Measurement ID:"
moreInfoTooltip="Can be found by clicking on your data stream in Google Analytics dashboard"
defaultValue={options?.trackingId ?? ''}
defaultValue={options?.trackingId}
placeholder="G-123456..."
onChange={updateTrackingId}
/>
<TextInput
label="Event action:"
defaultValue={options?.action ?? ''}
defaultValue={options?.action}
placeholder="Example: conversion"
onChange={updateAction}
/>
@@ -71,13 +71,13 @@ export const GoogleAnalyticsSettings = ({
<AccordionPanel pb={4} as={Stack} spacing="6">
<TextInput
label="Event category:"
defaultValue={options?.category ?? ''}
defaultValue={options?.category}
placeholder="Example: Typebot"
onChange={updateCategory}
/>
<TextInput
label="Event label:"
defaultValue={options?.label ?? ''}
defaultValue={options?.label}
placeholder="Example: Campaign Z"
onChange={updateLabel}
/>
@@ -91,7 +91,7 @@ export const GoogleAnalyticsSettings = ({
<TextInput
label="Send to:"
moreInfoTooltip="Useful to send a conversion event to Google Ads"
defaultValue={options?.sendTo?.toString() ?? ''}
defaultValue={options?.sendTo?.toString()}
placeholder="Example: AW-123456789"
onChange={updateSendTo}
/>

View File

@@ -1,11 +1,8 @@
import test from '@playwright/test'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import {
defaultGoogleAnalyticsOptions,
IntegrationBlockType,
} from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
test.describe('Google Analytics block', () => {
test('its configuration should work', async ({ page }) => {
@@ -15,7 +12,6 @@ test.describe('Google Analytics block', () => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: IntegrationBlockType.GOOGLE_ANALYTICS,
options: defaultGoogleAnalyticsOptions,
}),
},
])

View File

@@ -1,11 +1,12 @@
import React from 'react'
import { Stack, Text } from '@chakra-ui/react'
import { GoogleSheetsAction, GoogleSheetsOptions } from '@typebot.io/schemas'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { SetVariableLabel } from '@/components/SetVariableLabel'
import { GoogleSheetsBlock } from '@typebot.io/schemas'
import { GoogleSheetsAction } from '@typebot.io/schemas/features/blocks/integrations/googleSheets/constants'
type Props = {
options?: GoogleSheetsOptions
options?: GoogleSheetsBlock['options']
}
export const GoogleSheetsNodeContent = ({ options }: Props) => {
@@ -18,7 +19,7 @@ export const GoogleSheetsNodeContent = ({ options }: Props) => {
{typebot &&
options?.action === GoogleSheetsAction.GET &&
options?.cellsToExtract
.map((mapping) => mapping.variableId)
?.map((mapping) => mapping.variableId)
.map((variableId, idx) =>
variableId ? (
<SetVariableLabel

View File

@@ -12,16 +12,12 @@ import { DropdownList } from '@/components/DropdownList'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import {
Cell,
defaultGoogleSheetsGetOptions,
defaultGoogleSheetsInsertOptions,
defaultGoogleSheetsUpdateOptions,
ExtractingCell,
GoogleSheetsAction,
GoogleSheetsBlock,
GoogleSheetsGetOptions,
GoogleSheetsGetOptionsV6,
GoogleSheetsInsertRowOptions,
GoogleSheetsOptions,
GoogleSheetsUpdateRowOptions,
totalRowsToExtractOptions,
GoogleSheetsUpdateRowOptionsV6,
} from '@typebot.io/schemas'
import React, { useMemo } from 'react'
import { isDefined } from '@typebot.io/lib'
@@ -33,14 +29,18 @@ import { GoogleSheetConnectModal } from './GoogleSheetsConnectModal'
import { TableListItemProps, TableList } from '@/components/TableList'
import { CredentialsDropdown } from '@/features/credentials/components/CredentialsDropdown'
import { RowsFilterTableList } from './RowsFilterTableList'
import { createId } from '@paralleldrive/cuid2'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { useSheets } from '../hooks/useSheets'
import { Sheet } from '../types'
import {
GoogleSheetsAction,
defaultGoogleSheetsOptions,
totalRowsToExtractOptions,
} from '@typebot.io/schemas/features/blocks/integrations/googleSheets/constants'
type Props = {
options: GoogleSheetsOptions
onOptionsChange: (options: GoogleSheetsOptions) => void
options: GoogleSheetsBlock['options']
onOptionsChange: (options: GoogleSheetsBlock['options']) => void
blockId: string
}
@@ -70,36 +70,13 @@ export const GoogleSheetsSettings = ({
const handleSheetIdChange = (sheetId: string | undefined) =>
onOptionsChange({ ...options, sheetId })
const handleActionChange = (action: GoogleSheetsAction) => {
const baseOptions = {
credentialsId: options.credentialsId,
spreadsheetId: options.spreadsheetId,
sheetId: options.sheetId,
}
switch (action) {
case GoogleSheetsAction.GET: {
const newOptions: GoogleSheetsGetOptions = {
...baseOptions,
...defaultGoogleSheetsGetOptions(createId),
}
return onOptionsChange({ ...newOptions })
}
case GoogleSheetsAction.INSERT_ROW: {
const newOptions: GoogleSheetsInsertRowOptions = {
...baseOptions,
...defaultGoogleSheetsInsertOptions(createId),
}
return onOptionsChange({ ...newOptions })
}
case GoogleSheetsAction.UPDATE_ROW: {
const newOptions: GoogleSheetsUpdateRowOptions = {
...baseOptions,
...defaultGoogleSheetsUpdateOptions(createId),
}
return onOptionsChange({ ...newOptions })
}
}
}
const handleActionChange = (action: GoogleSheetsAction) =>
onOptionsChange({
credentialsId: options?.credentialsId,
spreadsheetId: options?.spreadsheetId,
sheetId: options?.sheetId,
action,
})
const handleCreateNewClick = async () => {
await save()
@@ -148,7 +125,7 @@ export const GoogleSheetsSettings = ({
placeholder="Select an operation"
/>
)}
{options.action && (
{options?.action && (
<ActionOptions
options={options}
sheet={sheet}
@@ -165,31 +142,40 @@ const ActionOptions = ({
onOptionsChange,
}: {
options:
| GoogleSheetsGetOptions
| GoogleSheetsGetOptionsV6
| GoogleSheetsInsertRowOptions
| GoogleSheetsUpdateRowOptions
| GoogleSheetsUpdateRowOptionsV6
sheet?: Sheet
onOptionsChange: (options: GoogleSheetsOptions) => void
onOptionsChange: (options: GoogleSheetsBlock['options']) => void
}) => {
const handleInsertColumnsChange = (cellsToInsert: Cell[]) =>
onOptionsChange({ ...options, cellsToInsert } as GoogleSheetsOptions)
onOptionsChange({
...options,
cellsToInsert,
} as GoogleSheetsBlock['options'])
const handleUpsertColumnsChange = (cellsToUpsert: Cell[]) =>
onOptionsChange({ ...options, cellsToUpsert } as GoogleSheetsOptions)
const handleReferenceCellChange = (referenceCell: Cell) =>
onOptionsChange({ ...options, referenceCell } as GoogleSheetsOptions)
onOptionsChange({
...options,
cellsToUpsert,
} as GoogleSheetsBlock['options'])
const handleExtractingCellsChange = (cellsToExtract: ExtractingCell[]) =>
onOptionsChange({ ...options, cellsToExtract } as GoogleSheetsOptions)
onOptionsChange({
...options,
cellsToExtract,
} as GoogleSheetsBlock['options'])
const handleFilterChange = (filter: GoogleSheetsGetOptions['filter']) =>
onOptionsChange({ ...options, filter } as GoogleSheetsOptions)
onOptionsChange({ ...options, filter } as GoogleSheetsBlock['options'])
const updateTotalRowsToExtract = (
totalRowsToExtract: GoogleSheetsGetOptions['totalRowsToExtract']
) =>
onOptionsChange({ ...options, totalRowsToExtract } as GoogleSheetsOptions)
onOptionsChange({
...options,
totalRowsToExtract,
} as GoogleSheetsBlock['options'])
const UpdatingCellItem = useMemo(
() =>
@@ -222,42 +208,22 @@ const ActionOptions = ({
case GoogleSheetsAction.UPDATE_ROW:
return (
<Accordion allowMultiple>
{options.referenceCell && (
<AccordionItem>
<AccordionButton>
<Text w="full" textAlign="left">
Row to update
</Text>
<AccordionIcon />
</AccordionButton>
<AccordionItem>
<AccordionButton>
<Text w="full" textAlign="left">
Row(s) to update
</Text>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pt="4">
<CellWithValueStack
columns={sheet?.columns ?? []}
item={options.referenceCell ?? { id: 'reference' }}
onItemChange={handleReferenceCellChange}
/>
</AccordionPanel>
</AccordionItem>
)}
{!options.referenceCell && (
<AccordionItem>
<AccordionButton>
<Text w="full" textAlign="left">
Row(s) to update
</Text>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pt="4">
<RowsFilterTableList
columns={sheet?.columns ?? []}
filter={options.filter}
onFilterChange={handleFilterChange}
/>
</AccordionPanel>
</AccordionItem>
)}
<AccordionPanel pt="4">
<RowsFilterTableList
columns={sheet?.columns ?? []}
filter={options.filter}
onFilterChange={handleFilterChange}
/>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton>
<Text w="full" textAlign="left">
@@ -281,49 +247,32 @@ const ActionOptions = ({
return (
<Accordion allowMultiple>
<Stack>
{options.referenceCell && (
<>
<AccordionItem>
<AccordionButton>
<Text w="full" textAlign="left">
Rows to select
Select row(s)
</Text>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pt="4">
<CellWithValueStack
<AccordionPanel pt="4" as={Stack}>
<DropdownList
items={totalRowsToExtractOptions}
currentItem={
options.totalRowsToExtract ??
defaultGoogleSheetsOptions.totalRowsToExtract
}
onItemSelect={updateTotalRowsToExtract}
/>
<RowsFilterTableList
columns={sheet?.columns ?? []}
item={options.referenceCell ?? { id: 'reference' }}
onItemChange={handleReferenceCellChange}
filter={options.filter}
onFilterChange={handleFilterChange}
/>
</AccordionPanel>
</AccordionItem>
)}
{!options.referenceCell && (
<>
<AccordionItem>
<AccordionButton>
<Text w="full" textAlign="left">
Select row(s)
</Text>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pt="4" as={Stack}>
<DropdownList
items={totalRowsToExtractOptions}
currentItem={options.totalRowsToExtract ?? 'All'}
onItemSelect={updateTotalRowsToExtract}
/>
<RowsFilterTableList
columns={sheet?.columns ?? []}
filter={options.filter}
onFilterChange={handleFilterChange}
/>
</AccordionPanel>
</AccordionItem>
</>
)}
</>
<AccordionItem>
<AccordionButton>
@@ -339,6 +288,7 @@ const ActionOptions = ({
onItemsChange={handleExtractingCellsChange}
Item={ExtractingCellItem}
addLabel="Add a value"
hasDefaultItem
/>
</AccordionPanel>
</AccordionItem>

View File

@@ -2,7 +2,8 @@ import { DropdownList } from '@/components/DropdownList'
import { TextInput } from '@/components/inputs'
import { TableListItemProps } from '@/components/TableList'
import { Stack } from '@chakra-ui/react'
import { ComparisonOperators, RowsFilterComparison } from '@typebot.io/schemas'
import { RowsFilterComparison } from '@typebot.io/schemas'
import { ComparisonOperators } from '@typebot.io/schemas/features/blocks/logic/condition/constants'
import React from 'react'
export const RowsFilterComparisonItem = ({

View File

@@ -3,11 +3,11 @@ import { TableList, TableListItemProps } from '@/components/TableList'
import { Flex } from '@chakra-ui/react'
import {
GoogleSheetsGetOptions,
LogicalOperator,
RowsFilterComparison,
} from '@typebot.io/schemas'
import React, { useCallback } from 'react'
import { RowsFilterComparisonItem } from './RowsFilterComparisonItem'
import { LogicalOperator } from '@typebot.io/schemas/features/blocks/logic/condition/constants'
type Props = {
filter: GoogleSheetsGetOptions['filter']

View File

@@ -7,7 +7,7 @@ type Props = {
}
export const MakeComContent = ({ block }: Props) => {
const webhook = block.options.webhook
const webhook = block.options?.webhook
if (isNotDefined(webhook?.body))
return <Text color="gray.500">Configure...</Text>

View File

@@ -1,12 +1,12 @@
import { Alert, AlertIcon, Button, Link, Stack, Text } from '@chakra-ui/react'
import { ExternalLinkIcon } from '@/components/icons'
import { MakeComBlock, Webhook, WebhookOptions } from '@typebot.io/schemas'
import { MakeComBlock, Webhook } from '@typebot.io/schemas'
import React from 'react'
import { WebhookAdvancedConfigForm } from '../../webhook/components/WebhookAdvancedConfigForm'
type Props = {
block: MakeComBlock
onOptionsChange: (options: WebhookOptions) => void
onOptionsChange: (options: MakeComBlock['options']) => void
}
export const MakeComSettings = ({
@@ -14,14 +14,13 @@ export const MakeComSettings = ({
onOptionsChange,
}: Props) => {
const setLocalWebhook = async (newLocalWebhook: Webhook) => {
if (!options.webhook) return
onOptionsChange({
...options,
webhook: newLocalWebhook,
})
}
const url = options.webhook?.url
const url = options?.webhook?.url
return (
<Stack spacing={4}>
@@ -43,15 +42,13 @@ export const MakeComSettings = ({
</Stack>
)}
</Alert>
{options.webhook && (
<WebhookAdvancedConfigForm
blockId={blockId}
webhook={options.webhook as Webhook}
options={options}
onWebhookChange={setLocalWebhook}
onOptionsChange={onOptionsChange}
/>
)}
<WebhookAdvancedConfigForm
blockId={blockId}
webhook={options?.webhook}
options={options}
onWebhookChange={setLocalWebhook}
onOptionsChange={onOptionsChange}
/>
</Stack>
)
}

View File

@@ -4,12 +4,10 @@ import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
import {
OpenAICredentials,
defaultBaseUrl,
} from '@typebot.io/schemas/features/blocks/integrations/openai'
import { OpenAICredentials } from '@typebot.io/schemas/features/blocks/integrations/openai'
import { isNotEmpty } from '@typebot.io/lib/utils'
import { OpenAI, ClientOptions } from 'openai'
import { defaultOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai/constants'
export const listModels = authenticatedProcedure
.meta({
@@ -25,7 +23,7 @@ export const listModels = authenticatedProcedure
z.object({
credentialsId: z.string(),
workspaceId: z.string(),
baseUrl: z.string().default(defaultBaseUrl),
baseUrl: z.string(),
apiVersion: z.string().optional(),
})
)
@@ -81,7 +79,7 @@ export const listModels = authenticatedProcedure
const config = {
apiKey: data.apiKey,
baseURL: baseUrl,
baseURL: baseUrl ?? defaultOpenAIOptions.baseUrl,
defaultHeaders: {
'api-key': data.apiKey,
},

View File

@@ -8,7 +8,7 @@ import {
} from '@typebot.io/schemas/features/blocks/integrations/openai'
type Props = {
task: OpenAIBlock['options']['task']
task: NonNullable<OpenAIBlock['options']>['task']
responseMapping:
| ChatCompletionOpenAIOptions['responseMapping']
| CreateImageOpenAIOptions['responseMapping']
@@ -24,7 +24,7 @@ export const OpenAINodeBody = ({ task, responseMapping }: Props) => {
</Text>
{typebot &&
responseMapping
.map((mapping) => mapping.variableId)
?.map((mapping) => mapping.variableId)
.map((variableId, idx) =>
variableId ? (
<SetVariableLabel

View File

@@ -13,17 +13,17 @@ import { CredentialsDropdown } from '@/features/credentials/components/Credentia
import {
ChatCompletionOpenAIOptions,
CreateImageOpenAIOptions,
defaultBaseUrl,
defaultChatCompletionOptions,
OpenAIBlock,
openAITasks,
} from '@typebot.io/schemas/features/blocks/integrations/openai'
import { OpenAICredentialsModal } from './OpenAICredentialsModal'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { DropdownList } from '@/components/DropdownList'
import { OpenAIChatCompletionSettings } from './createChatCompletion/OpenAIChatCompletionSettings'
import { createId } from '@paralleldrive/cuid2'
import { TextInput } from '@/components/inputs'
import {
defaultOpenAIOptions,
openAITasks,
} from '@typebot.io/schemas/features/blocks/integrations/openai/constants'
type OpenAITask = (typeof openAITasks)[number]
@@ -51,7 +51,7 @@ export const OpenAISettings = ({
case 'Create chat completion': {
onOptionsChange({
credentialsId: options?.credentialsId,
...defaultChatCompletionOptions(createId),
task,
})
break
}
@@ -72,6 +72,8 @@ export const OpenAISettings = ({
})
}
const baseUrl = options?.baseUrl ?? defaultOpenAIOptions.baseUrl
return (
<Stack>
{workspace && (
@@ -91,7 +93,7 @@ export const OpenAISettings = ({
/>
</>
)}
{options.credentialsId && (
{options?.credentialsId && (
<>
<Accordion allowToggle>
<AccordionItem>
@@ -104,10 +106,10 @@ export const OpenAISettings = ({
<AccordionPanel as={Stack} spacing={4}>
<TextInput
label="Base URL"
defaultValue={options.baseUrl}
defaultValue={baseUrl}
onChange={updateBaseUrl}
/>
{options.baseUrl !== defaultBaseUrl && (
{baseUrl !== defaultOpenAIOptions.baseUrl && (
<TextInput
label="API version"
defaultValue={options.apiVersion}

View File

@@ -1,16 +1,19 @@
import { DropdownList } from '@/components/DropdownList'
import { Textarea, TextInput } from '@/components/inputs'
import { Textarea } from '@/components/inputs'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { TableListItemProps } from '@/components/TableList'
import { Stack } from '@chakra-ui/react'
import { HStack, Stack, Text } from '@chakra-ui/react'
import { Variable } from '@typebot.io/schemas'
import { ChatCompletionOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai'
import {
chatCompletionMessageCustomRoles,
chatCompletionMessageRoles,
ChatCompletionOpenAIOptions,
} from '@typebot.io/schemas/features/blocks/integrations/openai'
deprecatedRoles,
} from '@typebot.io/schemas/features/blocks/integrations/openai/constants'
type Props = TableListItemProps<ChatCompletionOpenAIOptions['messages'][number]>
type Props = TableListItemProps<
NonNullable<ChatCompletionOpenAIOptions['messages']>[number]
>
const roles = [
...chatCompletionMessageCustomRoles,
@@ -27,75 +30,88 @@ export const ChatCompletionMessageItem = ({ item, onItemChange }: Props) => {
}
const changeSingleMessageContent = (content: string) => {
if (item.role === 'Messages sequence ✨') return
if (item.role === 'Messages sequence ✨' || item.role === 'Dialogue') return
onItemChange({ ...item, content })
}
const changeAssistantVariableId = (
const updateDialogueVariableId = (
variable: Pick<Variable, 'id'> | undefined
) => {
if (item.role !== 'Messages sequence ✨') return
onItemChange({
...item,
content: {
...item.content,
assistantMessagesVariableId: variable?.id,
},
})
if (item.role !== 'Dialogue') return
onItemChange({ ...item, dialogueVariableId: variable?.id })
}
const changeUserVariableId = (variable: Pick<Variable, 'id'> | undefined) => {
if (item.role !== 'Messages sequence ✨') return
onItemChange({
...item,
content: {
...item.content,
userMessagesVariableId: variable?.id,
},
})
}
const updateName = (name: string) => {
if (item.role === 'Messages sequence ✨') return
onItemChange({ ...item, name })
const updateStartsBy = (startsBy: 'user' | 'assistant') => {
if (item.role !== 'Dialogue') return
onItemChange({ ...item, startsBy })
}
return (
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
<DropdownList
currentItem={item.role}
items={roles}
items={roles.filter(
(role) =>
!deprecatedRoles.includes(role as (typeof deprecatedRoles)[number])
)}
onItemSelect={changeRole}
placeholder="Select type"
/>
{item.role === 'Messages sequence ✨' ? (
<>
<VariableSearchInput
initialVariableId={item.content?.userMessagesVariableId}
onSelectVariable={changeUserVariableId}
placeholder="User messages variable"
/>
<VariableSearchInput
initialVariableId={item.content?.assistantMessagesVariableId}
onSelectVariable={changeAssistantVariableId}
placeholder="Assistant messages variable"
/>
</>
) : (
<>
<Textarea
defaultValue={item.content}
onChange={changeSingleMessageContent}
placeholder="Content"
minH="150px"
/>
<TextInput
defaultValue={item.name}
onChange={updateName}
placeholder="Name (Optional)"
/>
</>
)}
<ChatCompletionMessageItemContent
item={item}
onChangeSingleMessageContent={changeSingleMessageContent}
onChangeDialogueVariableId={updateDialogueVariableId}
onStartsByChange={updateStartsBy}
/>
</Stack>
)
}
const ChatCompletionMessageItemContent = ({
onChangeSingleMessageContent,
onChangeDialogueVariableId,
onStartsByChange,
item,
}: {
onChangeSingleMessageContent: (content: string) => void
onChangeDialogueVariableId: (
variable: Pick<Variable, 'id'> | undefined
) => void
onStartsByChange: (startsBy: 'user' | 'assistant') => void
item: NonNullable<ChatCompletionOpenAIOptions['messages']>[number]
}) => {
switch (item.role) {
case 'assistant':
case 'user':
case 'system':
return (
<Textarea
defaultValue={item.content}
onChange={onChangeSingleMessageContent}
placeholder="Content"
minH="150px"
/>
)
case 'Dialogue':
return (
<Stack alignItems="flex-end">
<VariableSearchInput
initialVariableId={item.dialogueVariableId}
onSelectVariable={onChangeDialogueVariableId}
placeholder="Dialogue variable"
/>
<HStack>
<Text>starts by</Text>
<DropdownList
size="sm"
currentItem={item.startsBy ?? 'user'}
onItemSelect={onStartsByChange}
items={['user', 'assistant'] as const}
/>
</HStack>
</Stack>
)
case 'Messages sequence ✨':
return null
}
}

View File

@@ -3,13 +3,14 @@ import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { TableListItemProps } from '@/components/TableList'
import { Stack } from '@chakra-ui/react'
import { Variable } from '@typebot.io/schemas'
import { ChatCompletionOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai'
import {
ChatCompletionOpenAIOptions,
chatCompletionResponseValues,
} from '@typebot.io/schemas/features/blocks/integrations/openai'
defaultOpenAIResponseMappingItem,
} from '@typebot.io/schemas/features/blocks/integrations/openai/constants'
type Props = TableListItemProps<
ChatCompletionOpenAIOptions['responseMapping'][number]
NonNullable<ChatCompletionOpenAIOptions['responseMapping']>[number]
>
export const ChatCompletionResponseItem = ({ item, onItemChange }: Props) => {
@@ -26,7 +27,9 @@ export const ChatCompletionResponseItem = ({ item, onItemChange }: Props) => {
return (
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
<DropdownList
currentItem={item.valueToExtract ?? 'Message content'}
currentItem={
item.valueToExtract ?? defaultOpenAIResponseMappingItem.valueToExtract
}
items={chatCompletionResponseValues}
onItemSelect={changeValueToExtract}
/>

View File

@@ -2,12 +2,13 @@ import { Select } from '@/components/inputs/Select'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { useToast } from '@/hooks/useToast'
import { trpc } from '@/lib/trpc'
import { defaultOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai/constants'
type Props = {
baseUrl: string
baseUrl?: string
apiVersion?: string
credentialsId: string
defaultValue: string
defaultValue?: string
onChange: (model: string | undefined) => void
}
@@ -24,7 +25,7 @@ export const ModelsDropdown = ({
const { data } = trpc.openAI.listModels.useQuery(
{
credentialsId,
baseUrl,
baseUrl: baseUrl ?? defaultOpenAIOptions.baseUrl,
workspaceId: workspace?.id as string,
apiVersion,
},

View File

@@ -96,6 +96,7 @@ export const OpenAIChatCompletionSettings = ({
Item={ChatCompletionMessageItem}
onItemsChange={updateMessages}
isOrdered
hasDefaultItem
addLabel="Add message"
/>
</AccordionPanel>
@@ -132,6 +133,7 @@ export const OpenAIChatCompletionSettings = ({
Item={ChatCompletionResponseItem}
onItemsChange={updateResponseMapping}
newItemDefaultProps={{ valueToExtract: 'Message content' }}
hasDefaultItem
/>
</AccordionPanel>
</AccordionItem>

View File

@@ -1,9 +1,8 @@
import test, { expect } from '@playwright/test'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { createId } from '@paralleldrive/cuid2'
import { IntegrationBlockType } from '@typebot.io/schemas'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import { defaultBaseUrl } from '@typebot.io/schemas/features/blocks/integrations/openai'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
const typebotId = createId()
@@ -13,9 +12,6 @@ test('should be configurable', async ({ page }) => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: IntegrationBlockType.OPEN_AI,
options: {
baseUrl: defaultBaseUrl,
},
}),
},
])

View File

@@ -7,7 +7,7 @@ type Props = {
}
export const PabblyConnectContent = ({ block }: Props) => {
const webhook = block.options.webhook
const webhook = block.options?.webhook
if (isNotDefined(webhook?.body))
return <Text color="gray.500">Configure...</Text>

View File

@@ -1,17 +1,13 @@
import { Alert, AlertIcon, Button, Link, Stack, Text } from '@chakra-ui/react'
import { ExternalLinkIcon } from '@/components/icons'
import {
PabblyConnectBlock,
Webhook,
WebhookOptions,
} from '@typebot.io/schemas'
import { PabblyConnectBlock, Webhook } from '@typebot.io/schemas'
import React from 'react'
import { WebhookAdvancedConfigForm } from '../../webhook/components/WebhookAdvancedConfigForm'
import { TextInput } from '@/components/inputs'
type Props = {
block: PabblyConnectBlock
onOptionsChange: (options: WebhookOptions) => void
onOptionsChange: (options: PabblyConnectBlock['options']) => void
}
export const PabblyConnectSettings = ({
@@ -19,7 +15,6 @@ export const PabblyConnectSettings = ({
onOptionsChange,
}: Props) => {
const setLocalWebhook = async (newLocalWebhook: Webhook) => {
if (!options.webhook) return
onOptionsChange({
...options,
webhook: newLocalWebhook,
@@ -27,11 +22,10 @@ export const PabblyConnectSettings = ({
}
const updateUrl = (url: string) => {
if (!options.webhook) return
onOptionsChange({ ...options, webhook: { ...options.webhook, url } })
onOptionsChange({ ...options, webhook: { ...options?.webhook, url } })
}
const url = options.webhook?.url
const url = options?.webhook?.url
return (
<Stack spacing={4}>
@@ -60,15 +54,13 @@ export const PabblyConnectSettings = ({
withVariableButton={false}
debounceTimeout={0}
/>
{options.webhook && (
<WebhookAdvancedConfigForm
blockId={blockId}
webhook={options.webhook as Webhook}
options={options}
onWebhookChange={setLocalWebhook}
onOptionsChange={onOptionsChange}
/>
)}
<WebhookAdvancedConfigForm
blockId={blockId}
webhook={options?.webhook}
options={options}
onWebhookChange={setLocalWebhook}
onOptionsChange={onOptionsChange}
/>
</Stack>
)
}

View File

@@ -8,12 +8,12 @@ type Props = {
export const PixelNodeBody = ({ options }: Props) => (
<Text
color={options.eventType || options.pixelId ? 'currentcolor' : 'gray.500'}
color={options?.eventType || options?.pixelId ? 'currentcolor' : 'gray.500'}
noOfLines={1}
>
{options.eventType
{options?.eventType
? `Track "${options.eventType}"`
: options.pixelId
: options?.pixelId
? 'Init Pixel'
: 'Configure...'}
</Text>

View File

@@ -8,11 +8,12 @@ import { Select } from '@/components/inputs/Select'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { Stack, Text } from '@chakra-ui/react'
import { isDefined, isEmpty } from '@typebot.io/lib'
import { PixelBlock } from '@typebot.io/schemas'
import {
PixelBlock,
defaultPixelOptions,
pixelEventTypes,
pixelObjectProperties,
} from '@typebot.io/schemas'
} from '@typebot.io/schemas/features/blocks/integrations/pixel/constants'
import React, { useMemo } from 'react'
const pixelReferenceUrl =
@@ -23,7 +24,7 @@ type Props = {
onOptionsChange: (options: PixelBlock['options']) => void
}
type Item = NonNullable<PixelBlock['options']['params']>[number]
type Item = NonNullable<NonNullable<PixelBlock['options']>['params']>[number]
export const PixelSettings = ({ options, onOptionsChange }: Props) => {
const updateIsInitSkipped = (isChecked: boolean) =>
@@ -54,7 +55,7 @@ export const PixelSettings = ({ options, onOptionsChange }: Props) => {
eventType,
})
const updateParams = (params: PixelBlock['options']['params']) =>
const updateParams = (params: NonNullable<PixelBlock['options']>['params']) =>
onOptionsChange({
...options,
params,
@@ -87,7 +88,7 @@ export const PixelSettings = ({ options, onOptionsChange }: Props) => {
<SwitchWithLabel
label={'Skip initialization'}
moreInfoContent="Check this if the bot is embedded in your website and the pixel is already initialized."
initialValue={options?.isInitSkip ?? false}
initialValue={options?.isInitSkip ?? defaultPixelOptions.isInitSkip}
onCheckChange={updateIsInitSkipped}
/>
<SwitchWithRelatedSettings

View File

@@ -1,8 +1,8 @@
import test, { expect } from '@playwright/test'
import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import { IntegrationBlockType } from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
test.describe('Pixel block', () => {
test('its configuration should work', async ({ page }) => {
@@ -12,7 +12,6 @@ test.describe('Pixel block', () => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: IntegrationBlockType.PIXEL,
options: {},
}),
},
])

View File

@@ -6,14 +6,14 @@ type Props = {
}
export const SendEmailContent = ({ block }: Props) => {
if (block.options.recipients.length === 0)
if ((block.options?.recipients?.length ?? 0) === 0)
return <Text color="gray.500">Configure...</Text>
return (
<Wrap noOfLines={2} pr="6">
<WrapItem>
<Text>Send email to</Text>
</WrapItem>
{block.options.recipients.map((to) => (
{block.options?.recipients?.map((to) => (
<WrapItem key={to}>
<Tag>{to}</Tag>
</WrapItem>

View File

@@ -6,9 +6,14 @@ import {
HStack,
Switch,
FormLabel,
Accordion,
AccordionButton,
AccordionItem,
AccordionPanel,
AccordionIcon,
} from '@chakra-ui/react'
import { CodeEditor } from '@/components/inputs/CodeEditor'
import { SendEmailOptions, Variable } from '@typebot.io/schemas'
import { SendEmailBlock, Variable } from '@typebot.io/schemas'
import React from 'react'
import { isNotEmpty } from '@typebot.io/lib'
import { SmtpConfigModal } from './SmtpConfigModal'
@@ -19,10 +24,11 @@ import { TextInput, Textarea } from '@/components/inputs'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
import { env } from '@typebot.io/env'
import { defaultSendEmailOptions } from '@typebot.io/schemas/features/blocks/integrations/sendEmail/constants'
type Props = {
options: SendEmailOptions
onOptionsChange: (options: SendEmailOptions) => void
options: SendEmailBlock['options']
onOptionsChange: (options: SendEmailBlock['options']) => void
}
export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
@@ -96,7 +102,7 @@ export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
const handleIsBodyCodeChange = () =>
onOptionsChange({
...options,
isBodyCode: options.isBodyCode ? !options.isBodyCode : true,
isBodyCode: options?.isBodyCode ? !options.isBodyCode : true,
})
const handleChangeAttachmentVariable = (
@@ -115,7 +121,9 @@ export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
<CredentialsDropdown
type="smtp"
workspaceId={workspace.id}
currentCredentialsId={options.credentialsId}
currentCredentialsId={
options?.credentialsId ?? defaultSendEmailOptions.credentialsId
}
onCredentialsSelect={handleCredentialsSelect}
onCreateNewClick={onOpen}
defaultCredentialLabel={env.NEXT_PUBLIC_SMTP_FROM?.match(
@@ -125,42 +133,57 @@ export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
/>
)}
</Stack>
<TextInput
label="Reply to:"
onChange={handleReplyToChange}
defaultValue={options.replyTo}
placeholder={'email@gmail.com'}
/>
<TextInput
label="To:"
onChange={handleToChange}
defaultValue={options.recipients.join(', ')}
placeholder="email1@gmail.com, email2@gmail.com"
/>
<TextInput
label="Cc:"
onChange={handleCcChange}
defaultValue={options.cc?.join(', ') ?? ''}
placeholder="email1@gmail.com, email2@gmail.com"
/>
<TextInput
label="Bcc:"
onChange={handleBccChange}
defaultValue={options.bcc?.join(', ') ?? ''}
defaultValue={options?.recipients?.join(', ')}
placeholder="email1@gmail.com, email2@gmail.com"
/>
<Accordion allowToggle>
<AccordionItem>
<AccordionButton>
<HStack justifyContent="space-between" w="full">
<Text>Advanced</Text>
<AccordionIcon />
</HStack>
</AccordionButton>
<AccordionPanel as={Stack}>
<TextInput
label="Reply to:"
onChange={handleReplyToChange}
defaultValue={options?.replyTo}
placeholder={'email@gmail.com'}
/>
<TextInput
label="Cc:"
onChange={handleCcChange}
defaultValue={options?.cc?.join(', ') ?? ''}
placeholder="email1@gmail.com, email2@gmail.com"
/>
<TextInput
label="Bcc:"
onChange={handleBccChange}
defaultValue={options?.bcc?.join(', ') ?? ''}
placeholder="email1@gmail.com, email2@gmail.com"
/>
</AccordionPanel>
</AccordionItem>
</Accordion>
<TextInput
label="Subject:"
onChange={handleSubjectChange}
defaultValue={options.subject ?? ''}
defaultValue={options?.subject ?? ''}
/>
<SwitchWithLabel
label={'Custom content?'}
moreInfoContent="By default, the email body will be a recap of what has been collected so far. You can override it with this option."
initialValue={options.isCustomBody ?? false}
initialValue={
options?.isCustomBody ?? defaultSendEmailOptions.isCustomBody
}
onCheckChange={handleIsCustomBodyChange}
/>
{options.isCustomBody && (
{options?.isCustomBody && (
<Stack>
<Flex justifyContent="space-between">
<Text>Content: </Text>
@@ -168,7 +191,9 @@ export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
<Text fontSize="sm">Text</Text>
<Switch
size="sm"
isChecked={options.isBodyCode ?? false}
isChecked={
options.isBodyCode ?? defaultSendEmailOptions.isBodyCode
}
onChange={handleIsBodyCodeChange}
/>
<Text fontSize="sm">Code</Text>
@@ -188,24 +213,25 @@ export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
defaultValue={options.body ?? ''}
/>
)}
<Stack pb="4">
<HStack>
<FormLabel m="0" htmlFor="variable">
Attach files:
</FormLabel>
<MoreInfoTooltip>
The selected variable should have previously collected files
from the File upload input block.
</MoreInfoTooltip>
</HStack>
<VariableSearchInput
initialVariableId={options?.attachmentsVariableId}
onSelectVariable={handleChangeAttachmentVariable}
/>
</Stack>
</Stack>
)}
<Stack>
<HStack>
<FormLabel m="0" htmlFor="variable">
Attach files:
</FormLabel>
<MoreInfoTooltip>
The selected variable should have previously collected files from
the File upload input block.
</MoreInfoTooltip>
</HStack>
<VariableSearchInput
initialVariableId={options.attachmentsVariableId}
onSelectVariable={handleChangeAttachmentVariable}
/>
</Stack>
<SmtpConfigModal
isOpen={isOpen}
onClose={onClose}

View File

@@ -2,7 +2,7 @@ import prisma from '@typebot.io/lib/prisma'
import { canReadTypebots } from '@/helpers/databaseRules'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { Typebot } from '@typebot.io/schemas'
import { Block, Typebot } from '@typebot.io/schemas'
import { z } from 'zod'
import { fetchLinkedTypebots } from '@/features/blocks/logic/typebotLink/helpers/fetchLinkedTypebots'
import { parseResultExample } from '../helpers/parseResultExample'
@@ -45,14 +45,15 @@ export const getResultExample = authenticatedProcedure
groups: true,
edges: true,
variables: true,
events: true,
},
})) as Pick<Typebot, 'groups' | 'edges' | 'variables'> | null
})) as Pick<Typebot, 'groups' | 'edges' | 'variables' | 'events'> | null
if (!typebot)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
const block = typebot.groups
.flatMap((group) => group.blocks)
.flatMap<Block>((group) => group.blocks)
.find((block) => block.id === blockId)
if (!block)
@@ -65,6 +66,6 @@ export const getResultExample = authenticatedProcedure
typebot,
linkedTypebots,
userEmail: user.email ?? 'test@email.com',
})(block.groupId),
})(block.id),
}
})

View File

@@ -2,10 +2,11 @@ import prisma from '@typebot.io/lib/prisma'
import { canReadTypebots } from '@/helpers/databaseRules'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { Group, IntegrationBlockType, Typebot } from '@typebot.io/schemas'
import { byId, isWebhookBlock } from '@typebot.io/lib'
import { z } from 'zod'
import { Webhook } from '@typebot.io/prisma'
import { parseGroups } from '@typebot.io/schemas/features/typebot/group'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
import { Block } from '@typebot.io/schemas'
import { byId, isWebhookBlock } from '@typebot.io/lib'
export const listWebhookBlocks = authenticatedProcedure
.meta({
@@ -42,17 +43,22 @@ export const listWebhookBlocks = authenticatedProcedure
})
)
.query(async ({ input: { typebotId }, ctx: { user } }) => {
const typebot = (await prisma.typebot.findFirst({
const typebot = await prisma.typebot.findFirst({
where: canReadTypebots(typebotId, user),
select: {
version: true,
groups: true,
webhooks: true,
},
})) as (Pick<Typebot, 'groups'> & { webhooks: Webhook[] }) | null
})
if (!typebot)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
const webhookBlocks = (typebot?.groups as Group[]).reduce<
const groups = parseGroups(typebot.groups, {
typebotVersion: typebot.version,
})
const webhookBlocks = groups.reduce<
{
id: string
label: string
@@ -64,16 +70,17 @@ export const listWebhookBlocks = authenticatedProcedure
| IntegrationBlockType.PABBLY_CONNECT
}[]
>((webhookBlocks, group) => {
const blocks = group.blocks.filter(isWebhookBlock)
const blocks = (group.blocks as Block[]).filter(isWebhookBlock)
return [
...webhookBlocks,
...blocks.map((block) => ({
id: block.id,
type: block.type,
label: `${group.title} > ${block.id}`,
url: block.options.webhook
? block.options.webhook.url
: typebot?.webhooks.find(byId(block.webhookId))?.url ?? undefined,
url:
'webhookId' in block && !block.options?.webhook
? typebot?.webhooks.find(byId(block.webhookId))?.url ?? undefined
: block.options?.webhook?.url,
})),
]
}, [])

View File

@@ -2,10 +2,9 @@ import prisma from '@typebot.io/lib/prisma'
import { canWriteTypebots } from '@/helpers/databaseRules'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { Typebot, WebhookBlock } from '@typebot.io/schemas'
import { Block, WebhookBlock, parseGroups } from '@typebot.io/schemas'
import { byId, isWebhookBlock } from '@typebot.io/lib'
import { z } from 'zod'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums'
export const subscribeWebhook = authenticatedProcedure
.meta({
@@ -31,18 +30,23 @@ export const subscribeWebhook = authenticatedProcedure
})
)
.query(async ({ input: { typebotId, blockId, url }, ctx: { user } }) => {
const typebot = (await prisma.typebot.findFirst({
const typebot = await prisma.typebot.findFirst({
where: canWriteTypebots(typebotId, user),
select: {
version: true,
groups: true,
},
})) as Pick<Typebot, 'groups'> | null
})
if (!typebot)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
const webhookBlock = typebot.groups
.flatMap((g) => g.blocks)
const groups = parseGroups(typebot.groups, {
typebotVersion: typebot.version,
})
const webhookBlock = groups
.flatMap<Block>((g) => g.blocks)
.find(byId(blockId)) as WebhookBlock | null
if (!webhookBlock || !isWebhookBlock(webhookBlock))
@@ -51,29 +55,10 @@ export const subscribeWebhook = authenticatedProcedure
message: 'Webhook block not found',
})
const newWebhook = {
id: webhookBlock.webhookId ?? webhookBlock.id,
url,
body: '{{state}}',
method: HttpMethod.POST,
headers: [],
queryParams: [],
}
if (webhookBlock.webhookId)
await prisma.webhook.upsert({
where: { id: webhookBlock.webhookId },
update: { url, body: newWebhook.body, method: newWebhook.method },
create: {
typebotId,
...newWebhook,
},
})
else {
const updatedGroups = typebot.groups.map((group) =>
group.id !== webhookBlock.groupId
? group
: {
if (webhookBlock.options?.webhook || typebot.version === '6') {
const updatedGroups = groups.map((group) =>
group.blocks.some((b) => b.id === webhookBlock.id)
? {
...group,
blocks: group.blocks.map((block) =>
block.id !== webhookBlock.id
@@ -82,11 +67,15 @@ export const subscribeWebhook = authenticatedProcedure
...block,
options: {
...webhookBlock.options,
webhook: newWebhook,
webhook: {
...webhookBlock.options?.webhook,
url,
},
},
}
),
}
: group
)
await prisma.typebot.updateMany({
where: { id: typebotId },
@@ -94,6 +83,17 @@ export const subscribeWebhook = authenticatedProcedure
groups: updatedGroups,
},
})
} else {
if ('webhookId' in webhookBlock)
await prisma.webhook.update({
where: { id: webhookBlock.webhookId },
data: { url },
})
else
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Webhook block not found',
})
}
return {

View File

@@ -2,7 +2,7 @@ import prisma from '@typebot.io/lib/prisma'
import { canWriteTypebots } from '@/helpers/databaseRules'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { Typebot, Webhook, WebhookBlock } from '@typebot.io/schemas'
import { Block, WebhookBlock, parseGroups } from '@typebot.io/schemas'
import { byId, isWebhookBlock } from '@typebot.io/lib'
import { z } from 'zod'
@@ -29,19 +29,23 @@ export const unsubscribeWebhook = authenticatedProcedure
})
)
.query(async ({ input: { typebotId, blockId }, ctx: { user } }) => {
const typebot = (await prisma.typebot.findFirst({
const typebot = await prisma.typebot.findFirst({
where: canWriteTypebots(typebotId, user),
select: {
version: true,
groups: true,
webhooks: true,
},
})) as (Pick<Typebot, 'groups'> & { webhooks: Webhook[] }) | null
})
if (!typebot)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Typebot not found' })
const webhookBlock = typebot.groups
.flatMap((g) => g.blocks)
const groups = parseGroups(typebot.groups, {
typebotVersion: typebot.version,
})
const webhookBlock = groups
.flatMap<Block>((g) => g.blocks)
.find(byId(blockId)) as WebhookBlock | null
if (!webhookBlock || !isWebhookBlock(webhookBlock))
@@ -50,21 +54,10 @@ export const unsubscribeWebhook = authenticatedProcedure
message: 'Webhook block not found',
})
if (webhookBlock.webhookId)
await prisma.webhook.update({
where: { id: webhookBlock.webhookId },
data: { url: null },
})
else {
if (!webhookBlock.options.webhook)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Webhook block not found',
})
const updatedGroups = typebot.groups.map((group) =>
group.id !== webhookBlock.groupId
? group
: {
if (webhookBlock.options?.webhook || typebot.version === '6') {
const updatedGroups = groups.map((group) =>
group.blocks.some((b) => b.id === webhookBlock.id)
? {
...group,
blocks: group.blocks.map((block) =>
block.id !== webhookBlock.id
@@ -74,13 +67,14 @@ export const unsubscribeWebhook = authenticatedProcedure
options: {
...webhookBlock.options,
webhook: {
...webhookBlock.options.webhook,
...webhookBlock.options?.webhook,
url: undefined,
},
},
}
),
}
: group
)
await prisma.typebot.updateMany({
where: { id: typebotId },
@@ -88,6 +82,17 @@ export const unsubscribeWebhook = authenticatedProcedure
groups: updatedGroups,
},
})
} else {
if ('webhookId' in webhookBlock)
await prisma.webhook.update({
where: { id: webhookBlock.webhookId },
data: { url: null },
})
else
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Webhook block not found',
})
}
return {

View File

@@ -19,8 +19,8 @@ import {
KeyValue,
VariableForTest,
ResponseVariableMapping,
WebhookOptions,
Webhook,
WebhookBlock,
} from '@typebot.io/schemas'
import { useState, useMemo } from 'react'
import { executeWebhook } from '../queries/executeWebhookQuery'
@@ -30,14 +30,18 @@ import { QueryParamsInputs, HeadersInputs } from './KeyValueInputs'
import { DataVariableInputs } from './ResponseMappingInputs'
import { VariableForTestInputs } from './VariableForTestInputs'
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums'
import {
HttpMethod,
defaultWebhookAttributes,
defaultWebhookBlockOptions,
} from '@typebot.io/schemas/features/blocks/integrations/webhook/constants'
type Props = {
blockId: string
webhook: Webhook
options: WebhookOptions
webhook: Webhook | undefined
options: WebhookBlock['options']
onWebhookChange: (webhook: Webhook) => void
onOptionsChange: (options: WebhookOptions) => void
onOptionsChange: (options: WebhookBlock['options']) => void
}
export const WebhookAdvancedConfigForm = ({
@@ -80,12 +84,12 @@ export const WebhookAdvancedConfigForm = ({
const executeTestRequest = async () => {
if (!typebot) return
setIsTestResponseLoading(true)
if (!options.webhook) await save()
if (!options?.webhook) await save()
else await save()
const { data, error } = await executeWebhook(
typebot.id,
convertVariablesForTestToVariables(
options.variablesForTest,
options?.variablesForTest ?? [],
typebot.variables
),
{ blockId }
@@ -108,23 +112,34 @@ export const WebhookAdvancedConfigForm = ({
[responseKeys]
)
const isCustomBody =
options?.isCustomBody ?? defaultWebhookBlockOptions.isCustomBody
return (
<>
<SwitchWithRelatedSettings
label="Advanced configuration"
initialValue={options.isAdvancedConfig ?? true}
initialValue={
options?.isAdvancedConfig ??
defaultWebhookBlockOptions.isAdvancedConfig
}
onCheckChange={updateAdvancedConfig}
>
<SwitchWithLabel
label="Execute on client"
moreInfoContent="If enabled, the webhook will be executed on the client. It means it will be executed in the browser of your visitor. Make sure to enable CORS and do not expose sensitive data."
initialValue={options.isExecutedOnClient ?? false}
initialValue={
options?.isExecutedOnClient ??
defaultWebhookBlockOptions.isExecutedOnClient
}
onCheckChange={updateIsExecutedOnClient}
/>
<HStack justify="space-between">
<Text>Method:</Text>
<DropdownList
currentItem={webhook.method as HttpMethod}
currentItem={
(webhook?.method ?? defaultWebhookAttributes.method) as HttpMethod
}
onItemSelect={updateMethod}
items={Object.values(HttpMethod)}
/>
@@ -137,7 +152,7 @@ export const WebhookAdvancedConfigForm = ({
</AccordionButton>
<AccordionPanel pt="4">
<TableList<KeyValue>
initialItems={webhook.queryParams}
initialItems={webhook?.queryParams}
onItemsChange={updateQueryParams}
Item={QueryParamsInputs}
addLabel="Add a param"
@@ -151,7 +166,7 @@ export const WebhookAdvancedConfigForm = ({
</AccordionButton>
<AccordionPanel pt="4">
<TableList<KeyValue>
initialItems={webhook.headers}
initialItems={webhook?.headers}
onItemsChange={updateHeaders}
Item={HeadersInputs}
addLabel="Add a value"
@@ -166,12 +181,12 @@ export const WebhookAdvancedConfigForm = ({
<AccordionPanel py={4} as={Stack} spacing="6">
<SwitchWithLabel
label="Custom body"
initialValue={options.isCustomBody ?? true}
initialValue={isCustomBody}
onCheckChange={updateIsCustomBody}
/>
{(options.isCustomBody ?? true) && (
{isCustomBody && (
<CodeEditor
defaultValue={webhook.body ?? ''}
defaultValue={webhook?.body}
lang="json"
onChange={updateBody}
debounceTimeout={0}
@@ -186,7 +201,7 @@ export const WebhookAdvancedConfigForm = ({
</AccordionButton>
<AccordionPanel pt="4">
<TableList<VariableForTest>
initialItems={options?.variablesForTest ?? []}
initialItems={options?.variablesForTest}
onItemsChange={updateVariablesForTest}
Item={VariableForTestInputs}
addLabel="Add an entry"
@@ -195,7 +210,7 @@ export const WebhookAdvancedConfigForm = ({
</AccordionItem>
</Accordion>
</SwitchWithRelatedSettings>
{webhook.url && (
{webhook?.url && (
<Button
onClick={executeTestRequest}
colorScheme="blue"
@@ -207,7 +222,9 @@ export const WebhookAdvancedConfigForm = ({
{testResponse && (
<CodeEditor isReadOnly lang="json" value={testResponse} />
)}
{(testResponse || options.responseVariableMapping.length > 0) && (
{(testResponse ||
(options?.responseVariableMapping &&
options.responseVariableMapping.length > 0)) && (
<Accordion allowMultiple>
<AccordionItem>
<AccordionButton justifyContent="space-between">
@@ -216,7 +233,7 @@ export const WebhookAdvancedConfigForm = ({
</AccordionButton>
<AccordionPanel pt="4">
<TableList<ResponseVariableMapping>
initialItems={options.responseVariableMapping}
initialItems={options?.responseVariableMapping}
onItemsChange={updateResponseVariableMapping}
Item={ResponseMappingInputs}
addLabel="Add an entry"

View File

@@ -9,7 +9,7 @@ type Props = {
export const WebhookContent = ({ block: { options } }: Props) => {
const { typebot } = useTypebot()
const webhook = options.webhook
const webhook = options?.webhook
if (!webhook?.url) return <Text color="gray.500">Configure...</Text>
return (
@@ -17,8 +17,8 @@ export const WebhookContent = ({ block: { options } }: Props) => {
<Text noOfLines={2} pr="6">
{webhook.method} {webhook.url}
</Text>
{options.responseVariableMapping
.filter((mapping) => mapping.variableId)
{options?.responseVariableMapping
?.filter((mapping) => mapping.variableId)
.map((mapping) => (
<SetVariableLabel
key={mapping.variableId}

View File

@@ -1,12 +1,12 @@
import React from 'react'
import { Spinner, Stack } from '@chakra-ui/react'
import { WebhookOptions, Webhook, WebhookBlock } from '@typebot.io/schemas'
import { Stack } from '@chakra-ui/react'
import { Webhook, WebhookBlock } from '@typebot.io/schemas'
import { TextInput } from '@/components/inputs'
import { WebhookAdvancedConfigForm } from './WebhookAdvancedConfigForm'
type Props = {
block: WebhookBlock
onOptionsChange: (options: WebhookOptions) => void
onOptionsChange: (options: WebhookBlock['options']) => void
}
export const WebhookSettings = ({
@@ -14,28 +14,23 @@ export const WebhookSettings = ({
onOptionsChange,
}: Props) => {
const setLocalWebhook = async (newLocalWebhook: Webhook) => {
if (!options.webhook) return
onOptionsChange({ ...options, webhook: newLocalWebhook })
return
}
const updateUrl = (url: string) => {
if (!options.webhook) return
onOptionsChange({ ...options, webhook: { ...options.webhook, url } })
onOptionsChange({ ...options, webhook: { ...options?.webhook, url } })
}
if (!options.webhook) return <Spinner />
return (
<Stack spacing={4}>
<TextInput
placeholder="Paste webhook URL..."
defaultValue={options.webhook?.url ?? ''}
defaultValue={options?.webhook?.url}
onChange={updateUrl}
/>
<WebhookAdvancedConfigForm
blockId={blockId}
webhook={options.webhook as Webhook}
webhook={options?.webhook}
options={options}
onWebhookChange={setLocalWebhook}
onOptionsChange={onOptionsChange}

View File

@@ -1,7 +1,5 @@
import {
InputBlock,
InputBlockType,
LogicBlockType,
PublicTypebot,
ResultHeaderCell,
Block,
@@ -10,6 +8,9 @@ import {
} from '@typebot.io/schemas'
import { isInputBlock, byId, isNotDefined } from '@typebot.io/lib'
import { parseResultHeader } from '@typebot.io/lib/results'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
import { EventType } from '@typebot.io/schemas/features/events/constants'
export const parseResultExample =
({
@@ -17,12 +18,15 @@ export const parseResultExample =
linkedTypebots,
userEmail,
}: {
typebot: Pick<Typebot | PublicTypebot, 'groups' | 'variables' | 'edges'>
typebot: Pick<
Typebot | PublicTypebot,
'groups' | 'variables' | 'edges' | 'events'
>
linkedTypebots: (Typebot | PublicTypebot)[]
userEmail: string
}) =>
async (
currentGroupId: string
currentBlockId: string
): Promise<
{
message: 'This is a sample result, it has been generated ⬇️'
@@ -33,7 +37,7 @@ export const parseResultExample =
const linkedInputBlocks = await extractLinkedInputBlocks(
typebot,
linkedTypebots
)(currentGroupId)
)(currentBlockId)
return {
message: 'This is a sample result, it has been generated ⬇️',
@@ -49,12 +53,15 @@ export const parseResultExample =
const extractLinkedInputBlocks =
(
typebot:
| Pick<Typebot | PublicTypebot, 'groups' | 'variables' | 'edges'>
| Pick<
Typebot | PublicTypebot,
'groups' | 'variables' | 'edges' | 'events'
>
| undefined,
linkedTypebots: (Typebot | PublicTypebot)[]
) =>
async (
currentGroupId?: string,
blockId?: string,
direction: 'backward' | 'forward' = 'backward'
): Promise<InputBlock[]> => {
if (!typebot) return []
@@ -63,22 +70,28 @@ const extractLinkedInputBlocks =
direction,
typebot
)({
groupId: currentGroupId,
blockId,
}) as TypebotLinkBlock[]
const linkedBotInputs =
previousLinkedTypebotBlocks.length > 0
? await Promise.all(
previousLinkedTypebotBlocks.map((linkedBot) =>
extractLinkedInputBlocks(
linkedTypebots.find((t) =>
'typebotId' in t
? t.typebotId === linkedBot.options.typebotId
: t.id === linkedBot.options.typebotId
),
linkedTypebots
)(linkedBot.options.groupId, 'forward')
)
previousLinkedTypebotBlocks.map((linkedBot) => {
const typebot = linkedTypebots.find((t) =>
'typebotId' in t
? t.typebotId === linkedBot.options?.typebotId
: t.id === linkedBot.options?.typebotId
)
const blockId = linkedBot.options?.groupId
? typebot?.groups
.find(byId(linkedBot.options?.groupId))
?.blocks.at(0)?.id
: undefined
return extractLinkedInputBlocks(typebot, linkedTypebots)(
blockId,
'forward'
)
})
)
: []
@@ -88,7 +101,7 @@ const extractLinkedInputBlocks =
direction,
typebot
)({
groupId: currentGroupId,
blockId,
}) as InputBlock[]
).concat(linkedBotInputs.flatMap((l) => l))
}
@@ -133,9 +146,9 @@ const getSampleValue = ({
}) => {
switch (block.type) {
case InputBlockType.CHOICE:
return block.options.isMultipleChoice
? block.items.map((i) => i.content).join(', ')
: block.items[0]?.content ?? 'Item'
return block.options?.isMultipleChoice
? block.items?.map((i) => i.content).join(', ')
: block.items?.at(0)?.content ?? 'Item'
case InputBlockType.DATE:
return new Date().toUTCString()
case InputBlockType.EMAIL:
@@ -155,28 +168,42 @@ const walkEdgesAndExtract =
(
type: 'input' | 'linkedBot',
direction: 'backward' | 'forward',
typebot: Pick<Typebot | PublicTypebot, 'groups' | 'variables' | 'edges'>
typebot: Pick<
Typebot | PublicTypebot,
'groups' | 'variables' | 'edges' | 'events'
>
) =>
({ groupId }: { groupId?: string }): Block[] => {
({ blockId }: { blockId?: string }): Block[] => {
const groupId = typebot.groups.find((g) =>
g.blocks.some((b) => b.id === blockId)
)?.id
const startEventEdgeId = groupId
? undefined
: typebot.events?.find((e) => e.type === EventType.START)?.outgoingEdgeId
const currentGroupId =
groupId ??
(typebot.groups.find((b) => b.blocks[0].type === 'start')?.id as string)
(startEventEdgeId
? typebot.edges.find(byId(startEventEdgeId))?.to.groupId
: typebot.groups.find((g) => g.blocks[0].type === 'start')?.id)
if (!currentGroupId)
throw new Error("walkEdgesAndExtract - Can't find currentGroupId")
const blocksInGroup = extractBlocksInGroup(
type,
typebot
)({
groupId: currentGroupId,
blockId,
})
const otherGroupIds = getGroupIds(typebot, direction)(currentGroupId)
const otherGroupIds = getConnectedGroups(typebot, direction)(currentGroupId)
return [
...blocksInGroup,
...otherGroupIds.flatMap((groupId) =>
extractBlocksInGroup(type, typebot)({ groupId })
extractBlocksInGroup(type, typebot)({ groupId, blockId })
),
]
}
const getGroupIds =
const getConnectedGroups =
(
typebot: Pick<Typebot | PublicTypebot, 'groups' | 'variables' | 'edges'>,
direction: 'backward' | 'forward',
@@ -184,21 +211,26 @@ const getGroupIds =
) =>
(groupId: string): string[] => {
const groups = typebot.edges.reduce<string[]>((groupIds, edge) => {
const fromGroupId = typebot.groups.find((g) =>
g.blocks.some(
(b) => 'blockId' in edge.from && b.id === edge.from.blockId
)
)?.id
if (!fromGroupId) return groupIds
if (direction === 'forward')
return (!existingGroupIds ||
!existingGroupIds?.includes(edge.to.groupId)) &&
edge.from.groupId === groupId
fromGroupId === groupId
? [...groupIds, edge.to.groupId]
: groupIds
return (!existingGroupIds ||
!existingGroupIds.includes(edge.from.groupId)) &&
return (!existingGroupIds || !existingGroupIds.includes(fromGroupId)) &&
edge.to.groupId === groupId
? [...groupIds, edge.from.groupId]
? [...groupIds, fromGroupId]
: groupIds
}, [])
const newGroups = [...(existingGroupIds ?? []), ...groups]
return groups.concat(
groups.flatMap(getGroupIds(typebot, direction, newGroups))
groups.flatMap(getConnectedGroups(typebot, direction, newGroups))
)
}
@@ -207,7 +239,7 @@ const extractBlocksInGroup =
type: 'input' | 'linkedBot',
typebot: Pick<Typebot | PublicTypebot, 'groups' | 'variables' | 'edges'>
) =>
({ groupId, blockId }: { groupId: string; blockId?: string }) => {
({ groupId, blockId }: { groupId: string; blockId: string | undefined }) => {
const currentGroup = typebot.groups.find(byId(groupId))
if (!currentGroup) return []
const blocks: Block[] = []

View File

@@ -3,11 +3,11 @@ import {
createWebhook,
importTypebotInDatabase,
} from '@typebot.io/lib/playwright/databaseActions'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/enums'
import { createId } from '@paralleldrive/cuid2'
import { getTestAsset } from '@/test/utils/playwright'
import { apiToken } from '@typebot.io/lib/playwright/databaseSetup'
import { env } from '@typebot.io/env'
import { HttpMethod } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants'
test.describe('Builder', () => {
test('easy configuration should work', async ({ page }) => {

View File

@@ -7,7 +7,7 @@ type Props = {
}
export const ZapierContent = ({ block }: Props) => {
const webhook = block.options.webhook
const webhook = block.options?.webhook
if (isNotDefined(webhook?.body))
return <Text color="gray.500">Configure...</Text>

View File

@@ -1,12 +1,12 @@
import { Alert, AlertIcon, Button, Link, Stack, Text } from '@chakra-ui/react'
import { ExternalLinkIcon } from '@/components/icons'
import { Webhook, WebhookOptions, ZapierBlock } from '@typebot.io/schemas'
import { Webhook, WebhookBlock, ZapierBlock } from '@typebot.io/schemas'
import React from 'react'
import { WebhookAdvancedConfigForm } from '../../webhook/components/WebhookAdvancedConfigForm'
type Props = {
block: ZapierBlock
onOptionsChange: (options: WebhookOptions) => void
onOptionsChange: (options: WebhookBlock['options']) => void
}
export const ZapierSettings = ({
@@ -14,7 +14,6 @@ export const ZapierSettings = ({
onOptionsChange,
}: Props) => {
const setLocalWebhook = async (newLocalWebhook: Webhook) => {
if (!options.webhook) return
onOptionsChange({
...options,
webhook: newLocalWebhook,
@@ -22,7 +21,7 @@ export const ZapierSettings = ({
return
}
const url = options.webhook?.url
const url = options?.webhook?.url
return (
<Stack spacing={4}>
@@ -44,15 +43,13 @@ export const ZapierSettings = ({
</Stack>
)}
</Alert>
{options.webhook && (
<WebhookAdvancedConfigForm
blockId={blockId}
webhook={options.webhook as Webhook}
options={options}
onWebhookChange={setLocalWebhook}
onOptionsChange={onOptionsChange}
/>
)}
<WebhookAdvancedConfigForm
blockId={blockId}
webhook={options?.webhook}
options={options}
onWebhookChange={setLocalWebhook}
onOptionsChange={onOptionsChange}
/>
</Stack>
)
}

Some files were not shown because too many files have changed in this diff Show More