@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
}
|
||||
})
|
||||
@@ -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,
|
||||
})),
|
||||
}
|
||||
})
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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 },
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'>) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -7,7 +7,7 @@ type Props = {
|
||||
|
||||
export const PaymentInputContent = ({ block }: Props) => {
|
||||
if (
|
||||
!block.options.amount ||
|
||||
!block.options?.amount ||
|
||||
!block.options.credentialsId ||
|
||||
!block.options.currency
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 },
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {},
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
]
|
||||
}, [])
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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[] = []
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user