feat(bot): ✨ Support variables in buttons
This commit is contained in:
@@ -0,0 +1,132 @@
|
|||||||
|
{
|
||||||
|
"id": "cl4fr6kca0000p11abjka8lvd",
|
||||||
|
"createdAt": "2022-06-15T15:33:43.930Z",
|
||||||
|
"updatedAt": "2022-06-15T15:36:44.821Z",
|
||||||
|
"icon": null,
|
||||||
|
"name": "My typebot",
|
||||||
|
"publishedTypebotId": null,
|
||||||
|
"folderId": null,
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"id": "block0",
|
||||||
|
"title": "Group #0",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "block0",
|
||||||
|
"type": "start",
|
||||||
|
"label": "Start",
|
||||||
|
"groupId": "block0",
|
||||||
|
"outgoingEdgeId": "edge1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"graphCoordinates": { "x": 0, "y": 0 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "block1",
|
||||||
|
"title": "Group #1",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "cl4fr6rgf0000396ml1ai0t8v",
|
||||||
|
"type": "Set variable",
|
||||||
|
"groupId": "block1",
|
||||||
|
"options": {
|
||||||
|
"variableId": "vcl4fr8f8l000b396m6gsbnrmd",
|
||||||
|
"expressionToEvaluate": "Variable item"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "block1",
|
||||||
|
"type": "choice input",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "choice1",
|
||||||
|
"type": 0,
|
||||||
|
"blockId": "block1",
|
||||||
|
"content": "Item 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cl4fr7e6i0003396mkh7mol65",
|
||||||
|
"type": 0,
|
||||||
|
"blockId": "block1",
|
||||||
|
"content": "{{Item 2}}",
|
||||||
|
"outgoingEdgeId": "cl4fr80900009396my6euvunj"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cl4fr7lr90004396mh9vw8wnq",
|
||||||
|
"type": 0,
|
||||||
|
"blockId": "block1",
|
||||||
|
"content": "Item 3"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"groupId": "block1",
|
||||||
|
"options": { "buttonLabel": "Send", "isMultipleChoice": false }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"graphCoordinates": { "x": 199, "y": 210 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cl4fr7wsv0007396m7xgbeymx",
|
||||||
|
"title": "Group #2",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"id": "cl4fr7wsv0008396mf9oi9lvi",
|
||||||
|
"type": "text",
|
||||||
|
"content": {
|
||||||
|
"html": "<div>Ok great!</div>",
|
||||||
|
"richText": [
|
||||||
|
{ "type": "p", "children": [{ "text": "Ok great!" }] }
|
||||||
|
],
|
||||||
|
"plainText": "Ok great!"
|
||||||
|
},
|
||||||
|
"groupId": "cl4fr7wsv0007396m7xgbeymx"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"graphCoordinates": { "x": 603, "y": 195 }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variables": [{ "id": "vcl4fr8f8l000b396m6gsbnrmd", "name": "Item 2" }],
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"id": "edge1",
|
||||||
|
"to": { "groupId": "block1" },
|
||||||
|
"from": { "blockId": "block0", "groupId": "block0" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cl4fr80900009396my6euvunj",
|
||||||
|
"to": { "groupId": "cl4fr7wsv0007396m7xgbeymx" },
|
||||||
|
"from": {
|
||||||
|
"itemId": "cl4fr7e6i0003396mkh7mol65",
|
||||||
|
"blockId": "block1",
|
||||||
|
"groupId": "block1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme": {
|
||||||
|
"chat": {
|
||||||
|
"inputs": {
|
||||||
|
"color": "#303235",
|
||||||
|
"backgroundColor": "#FFFFFF",
|
||||||
|
"placeholderColor": "#9095A0"
|
||||||
|
},
|
||||||
|
"buttons": { "color": "#FFFFFF", "backgroundColor": "#0042DA" },
|
||||||
|
"hostBubbles": { "color": "#303235", "backgroundColor": "#F7F8FF" },
|
||||||
|
"guestBubbles": { "color": "#FFFFFF", "backgroundColor": "#FF8E21" }
|
||||||
|
},
|
||||||
|
"general": { "font": "Open Sans", "background": { "type": "None" } }
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"general": {
|
||||||
|
"isBrandingEnabled": true,
|
||||||
|
"isInputPrefillEnabled": true,
|
||||||
|
"isHideQueryParamsEnabled": true,
|
||||||
|
"isNewResultOnRefreshEnabled": false
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"description": "Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form."
|
||||||
|
},
|
||||||
|
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
|
||||||
|
},
|
||||||
|
"publicId": null,
|
||||||
|
"customDomain": null,
|
||||||
|
"workspaceId": "proWorkspace"
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import test, { expect } from '@playwright/test'
|
import test, { expect } from '@playwright/test'
|
||||||
import {
|
import {
|
||||||
createTypebots,
|
createTypebots,
|
||||||
|
importTypebotInDatabase,
|
||||||
parseDefaultGroupWithBlock,
|
parseDefaultGroupWithBlock,
|
||||||
} from '../../services/database'
|
} from '../../services/database'
|
||||||
import { defaultChoiceInputOptions, InputBlockType, ItemType } from 'models'
|
import { defaultChoiceInputOptions, InputBlockType, ItemType } from 'models'
|
||||||
import { typebotViewer } from '../../services/selectorUtils'
|
import { typebotViewer } from '../../services/selectorUtils'
|
||||||
import cuid from 'cuid'
|
import cuid from 'cuid'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
test.describe.parallel('Buttons input block', () => {
|
test.describe.parallel('Buttons input block', () => {
|
||||||
test('can edit button items', async ({ page }) => {
|
test('can edit button items', async ({ page }) => {
|
||||||
@@ -67,3 +69,30 @@ test.describe.parallel('Buttons input block', () => {
|
|||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Variable buttons should work', async ({ page }) => {
|
||||||
|
const typebotId = cuid()
|
||||||
|
await importTypebotInDatabase(
|
||||||
|
path.join(__dirname, '../../fixtures/typebots/inputs/variableButton.json'),
|
||||||
|
{
|
||||||
|
id: typebotId,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
|
await page.click('text=Preview')
|
||||||
|
await typebotViewer(page).locator('text=Variable item').click()
|
||||||
|
await expect(typebotViewer(page).locator('text=Variable item')).toBeVisible()
|
||||||
|
await expect(typebotViewer(page).locator('text=Ok great!')).toBeVisible()
|
||||||
|
await page.click('text="Item 1"')
|
||||||
|
await page.fill('input[value="Item 1"]', '{{Item 2}}')
|
||||||
|
await page.click('[data-testid="block1-icon"]')
|
||||||
|
await page.click('text=Multiple choice?')
|
||||||
|
await page.click('text="Restart"')
|
||||||
|
await typebotViewer(page).locator('text="Variable item" >> nth=0').click()
|
||||||
|
await typebotViewer(page).locator('text="Variable item" >> nth=1').click()
|
||||||
|
await typebotViewer(page).locator('text="Send"').click()
|
||||||
|
await expect(
|
||||||
|
typebotViewer(page).locator('text="Variable item, Variable item"')
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ import { PaymentForm } from './inputs/PaymentForm'
|
|||||||
import { RatingForm } from './inputs/RatingForm'
|
import { RatingForm } from './inputs/RatingForm'
|
||||||
import { FileUploadForm } from './inputs/FileUploadForm'
|
import { FileUploadForm } from './inputs/FileUploadForm'
|
||||||
|
|
||||||
export type InputSubmitContent = { label?: string; value: string }
|
export type InputSubmitContent = {
|
||||||
|
label?: string
|
||||||
|
value: string
|
||||||
|
itemId?: string
|
||||||
|
}
|
||||||
|
|
||||||
export const InputChatBlock = ({
|
export const InputChatBlock = ({
|
||||||
block,
|
block,
|
||||||
@@ -40,7 +44,7 @@ export const InputChatBlock = ({
|
|||||||
? variableId && typebot.variables.find(byId(variableId))?.value
|
? variableId && typebot.variables.find(byId(variableId))?.value
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const handleSubmit = async ({ label, value }: InputSubmitContent) => {
|
const handleSubmit = async ({ label, value, itemId }: InputSubmitContent) => {
|
||||||
setAnswer(label ?? value)
|
setAnswer(label ?? value)
|
||||||
const isRetry = !isInputValid(value, block.type)
|
const isRetry = !isInputValid(value, block.type)
|
||||||
if (!isRetry && addAnswer)
|
if (!isRetry && addAnswer)
|
||||||
@@ -50,7 +54,7 @@ export const InputChatBlock = ({
|
|||||||
content: value,
|
content: value,
|
||||||
variableId: variableId ?? null,
|
variableId: variableId ?? null,
|
||||||
})
|
})
|
||||||
if (!isEditting) onTransitionEnd({ label, value }, isRetry)
|
if (!isEditting) onTransitionEnd({ label, value, itemId }, isRetry)
|
||||||
setIsEditting(false)
|
setIsEditting(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useAnswers } from 'contexts/AnswersContext'
|
import { useAnswers } from 'contexts/AnswersContext'
|
||||||
|
import { useTypebot } from 'contexts/TypebotContext'
|
||||||
import { ChoiceInputBlock } from 'models'
|
import { ChoiceInputBlock } from 'models'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
import { parseVariables } from 'services/variable'
|
||||||
import { InputSubmitContent } from '../InputChatBlock'
|
import { InputSubmitContent } from '../InputChatBlock'
|
||||||
import { SendButton } from './SendButton'
|
import { SendButton } from './SendButton'
|
||||||
|
|
||||||
@@ -10,13 +12,20 @@ type ChoiceFormProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ChoiceForm = ({ block, onSubmit }: ChoiceFormProps) => {
|
export const ChoiceForm = ({ block, onSubmit }: ChoiceFormProps) => {
|
||||||
|
const {
|
||||||
|
typebot: { variables },
|
||||||
|
} = useTypebot()
|
||||||
const { resultValues } = useAnswers()
|
const { resultValues } = useAnswers()
|
||||||
const [selectedIndices, setSelectedIndices] = useState<number[]>([])
|
const [selectedIndices, setSelectedIndices] = useState<number[]>([])
|
||||||
|
|
||||||
const handleClick = (itemIndex: number) => (e: React.MouseEvent) => {
|
const handleClick = (itemIndex: number) => (e: React.MouseEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (block.options?.isMultipleChoice) toggleSelectedItemIndex(itemIndex)
|
if (block.options?.isMultipleChoice) toggleSelectedItemIndex(itemIndex)
|
||||||
else onSubmit({ value: block.items[itemIndex].content ?? '' })
|
else
|
||||||
|
onSubmit({
|
||||||
|
value: parseVariables(variables)(block.items[itemIndex].content),
|
||||||
|
itemId: block.items[itemIndex].id,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleSelectedItemIndex = (itemIndex: number) => {
|
const toggleSelectedItemIndex = (itemIndex: number) => {
|
||||||
@@ -32,7 +41,9 @@ export const ChoiceForm = ({ block, onSubmit }: ChoiceFormProps) => {
|
|||||||
const handleSubmit = () =>
|
const handleSubmit = () =>
|
||||||
onSubmit({
|
onSubmit({
|
||||||
value: selectedIndices
|
value: selectedIndices
|
||||||
.map((itemIndex) => block.items[itemIndex].content)
|
.map((itemIndex) =>
|
||||||
|
parseVariables(variables)(block.items[itemIndex].content)
|
||||||
|
)
|
||||||
.join(', '),
|
.join(', '),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -59,7 +70,7 @@ export const ChoiceForm = ({ block, onSubmit }: ChoiceFormProps) => {
|
|||||||
data-testid="button"
|
data-testid="button"
|
||||||
data-itemid={item.id}
|
data-itemid={item.id}
|
||||||
>
|
>
|
||||||
{item.content}
|
{parseVariables(variables)(item.content)}
|
||||||
</button>
|
</button>
|
||||||
{isUniqueFirstButton && (
|
{isUniqueFirstButton && (
|
||||||
<span className="flex h-3 w-3 absolute top-0 right-0 -mt-1 -mr-1 ping">
|
<span className="flex h-3 w-3 absolute top-0 right-0 -mt-1 -mr-1 ping">
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
isInputBlock,
|
isInputBlock,
|
||||||
isIntegrationBlock,
|
isIntegrationBlock,
|
||||||
isLogicBlock,
|
isLogicBlock,
|
||||||
|
byId,
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
import { executeLogic } from 'services/logic'
|
import { executeLogic } from 'services/logic'
|
||||||
import { executeIntegration } from 'services/integration'
|
import { executeIntegration } from 'services/integration'
|
||||||
@@ -187,7 +188,7 @@ export const ChatGroup = ({
|
|||||||
isChoiceInput(currentBlock) && !currentBlock.options.isMultipleChoice
|
isChoiceInput(currentBlock) && !currentBlock.options.isMultipleChoice
|
||||||
if (isSingleChoiceBlock) {
|
if (isSingleChoiceBlock) {
|
||||||
const nextEdgeId = currentBlock.items.find(
|
const nextEdgeId = currentBlock.items.find(
|
||||||
(i) => i.content === answerContent?.value
|
byId(answerContent?.itemId)
|
||||||
)?.outgoingEdgeId
|
)?.outgoingEdgeId
|
||||||
if (nextEdgeId) return onGroupEnd({ edgeId: nextEdgeId })
|
if (nextEdgeId) return onGroupEnd({ edgeId: nextEdgeId })
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user