2
0

(variables) Add session option in variables (#1490)

Closes #1488
This commit is contained in:
Baptiste Arnaud
2024-05-02 15:11:16 +02:00
committed by GitHub
parent 87653f8e10
commit b4ae098440
20 changed files with 156 additions and 82 deletions

View File

@ -11,13 +11,6 @@
"editor.formatOnSave": true,
"editor.tabSize": 2,
"typescript.updateImportsOnFileMove.enabled": "always",
"playwright.env": {
"DATABASE_URL": "postgresql://postgres:typebot@127.0.0.1:5432/typebot",
"NEXT_PUBLIC_VIEWER_URL": "http://localhost:3001",
"NEXTAUTH_URL": "http://localhost:3000",
"ENCRYPTION_SECRET": "H+KbL/OFrqbEuDy/1zX8bsPG+spXri3S",
"S3_ENDPOINT": "http://localhost:9000"
},
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"
}

View File

@ -99,7 +99,7 @@
},
"devDependencies": {
"@chakra-ui/styled-system": "2.9.1",
"@playwright/test": "1.41.2",
"@playwright/test": "1.43.1",
"@typebot.io/billing": "workspace:*",
"@typebot.io/forge": "workspace:*",
"@typebot.io/forge-repository": "workspace:*",
@ -123,12 +123,13 @@
"@types/qs": "6.9.7",
"@types/react": "18.2.15",
"@types/tinycolor2": "1.4.3",
"dotenv-cli": "7.2.1",
"dotenv-cli": "7.4.1",
"eslint": "8.44.0",
"eslint-config-custom": "workspace:*",
"next-runtime-env": "1.6.2",
"superjson": "1.12.4",
"typescript": "5.4.5",
"zod": "3.22.4"
"zod": "3.22.4",
"dotenv": "16.4.5"
}
}

View File

@ -1,4 +1,8 @@
import { defineConfig, devices } from '@playwright/test'
import { resolve } from 'path'
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('dotenv').config({ path: resolve(__dirname, '../../.env') })
export default defineConfig({
timeout: process.env.CI ? 50 * 1000 : 40 * 1000,

View File

@ -6,8 +6,9 @@ import {
Switch,
SwitchProps,
} from '@chakra-ui/react'
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import { MoreInfoTooltip } from '../MoreInfoTooltip'
import { isDefined } from '@typebot.io/lib'
export type SwitchWithLabelProps = {
label: string
@ -32,6 +33,11 @@ export const SwitchWithLabel = ({
if (onCheckChange) onCheckChange(!isChecked)
}
useEffect(() => {
if (isChecked === undefined && isDefined(initialValue))
setIsChecked(initialValue)
}, [initialValue, isChecked])
return (
<FormControl as={HStack} justifyContent={justifyContent}>
<FormLabel mb="0">

View File

@ -123,7 +123,7 @@ export const VariableSearchInput = ({
if (!inputValue || inputValue === '') return
const id = 'v' + createId()
onSelectVariable({ id, name: inputValue })
createVariable({ id, name: inputValue })
createVariable({ id, name: inputValue, isSessionVariable: true })
inputRef.current?.blur()
onClose()
}

View File

@ -24,7 +24,7 @@ test.describe('Date input block', () => {
'date'
)
await page.locator('[data-testid="from-date"]').fill('2021-01-01')
await page.getByRole('button', { name: 'Send' }).click()
await page.locator('form').getByRole('button').click()
await expect(page.locator('text="01/01/2021"')).toBeVisible()
await page.click(`text=Pick a date`)

View File

@ -32,7 +32,7 @@ test('options should work', async ({ page }) => {
await page.click('text=Test')
await expect(page.locator(`text=Send`)).toBeHidden()
await page.getByRole('button', { name: '8' }).click()
await page.getByRole('checkbox', { name: '8' }).click()
await page.locator(`text=Send`).click()
await expect(page.getByTestId('guest-bubble')).toHaveText('8')
await page.click('text=Rate from 0 to 10')

View File

@ -1,7 +1,6 @@
import { Alert, AlertIcon, FormLabel, Stack, Tag, Text } from '@chakra-ui/react'
import { CodeEditor } from '@/components/inputs/CodeEditor'
import { SetVariableBlock, Variable } from '@typebot.io/schemas'
import React from 'react'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { Select } from '@/components/inputs/Select'
@ -13,6 +12,7 @@ import {
} from '@typebot.io/schemas/features/blocks/logic/setVariable/constants'
import { TextInput } from '@/components/inputs'
import { isDefined } from '@typebot.io/lib'
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
type Props = {
options: SetVariableBlock['options']
@ -24,7 +24,12 @@ const setVarTypes = valueTypes.filter(
)
export const SetVariableSettings = ({ options, onOptionsChange }: Props) => {
const updateVariableId = (variable?: Variable) =>
const { typebot, updateVariable } = useTypebot()
const selectedVariable = typebot?.variables.find(
(variable) => variable.id === options?.variableId
)
const updateVariableId = (variable?: Pick<Variable, 'id'>) =>
onOptionsChange({
...options,
variableId: variable?.id,
@ -36,6 +41,13 @@ export const SetVariableSettings = ({ options, onOptionsChange }: Props) => {
type: type as NonNullable<SetVariableBlock['options']>['type'],
})
const updateIsSessionVariable = (isSavingInResults: boolean) => {
if (!selectedVariable?.id) return
updateVariable(selectedVariable.id, {
isSessionVariable: !isSavingInResults,
})
}
return (
<Stack spacing={4}>
<Stack>
@ -49,22 +61,34 @@ export const SetVariableSettings = ({ options, onOptionsChange }: Props) => {
/>
</Stack>
<Stack>
<Text mb="0" fontWeight="medium">
Value:
</Text>
<Select
selectedItem={options?.type ?? defaultSetVariableOptions.type}
items={setVarTypes.map((type) => ({
label: type,
value: type,
icon:
type === 'Contact name' || type === 'Phone number' ? (
<WhatsAppLogo />
) : undefined,
}))}
onSelect={updateValueType}
/>
<Stack spacing="4">
<Stack>
<Text mb="0" fontWeight="medium">
Value:
</Text>
<Select
selectedItem={options?.type ?? defaultSetVariableOptions.type}
items={setVarTypes.map((type) => ({
label: type,
value: type,
icon:
type === 'Contact name' || type === 'Phone number' ? (
<WhatsAppLogo />
) : undefined,
}))}
onSelect={updateValueType}
/>
</Stack>
{selectedVariable && (
<SwitchWithLabel
key={selectedVariable.id}
label="Save in results?"
moreInfoContent="By default, the variable is saved only for the user chat session. Check this option if you want to also store the variable in the typebot Results table."
initialValue={!selectedVariable.isSessionVariable}
onCheckChange={updateIsSessionVariable}
/>
)}
<SetVariableValue options={options} onOptionsChange={onOptionsChange} />
</Stack>
</Stack>
@ -91,7 +115,7 @@ const SetVariableValue = ({
isExecutedOnClient,
})
const updateItemVariableId = (variable?: Variable) => {
const updateItemVariableId = (variable?: Pick<Variable, 'id'>) => {
if (!options || options.type !== 'Map item with same index') return
onOptionsChange({
...options,
@ -102,7 +126,7 @@ const SetVariableValue = ({
})
}
const updateBaseListVariableId = (variable?: Variable) => {
const updateBaseListVariableId = (variable?: Pick<Variable, 'id'>) => {
if (!options || options.type !== 'Map item with same index') return
onOptionsChange({
...options,
@ -113,7 +137,7 @@ const SetVariableValue = ({
})
}
const updateTargetListVariableId = (variable?: Variable) => {
const updateTargetListVariableId = (variable?: Pick<Variable, 'id'>) => {
if (!options || options.type !== 'Map item with same index') return
onOptionsChange({
...options,

View File

@ -28,11 +28,16 @@ test.describe('Set variable block', () => {
.fill('1000 * {{Num}}')
await page.click('text=Click to edit...', { force: true })
await expect(page.getByText('Save in results?')).toBeHidden()
await page.fill(
'input[placeholder="Select a variable"] >> nth=-1',
'Custom var'
)
await page.getByRole('menuitem', { name: 'Create Custom var' }).click()
await expect(page.getByText('Save in results?')).toBeVisible()
await expect(
page.getByRole('group').nth(1).locator('.chakra-switch')
).not.toHaveAttribute('data-checked')
await page
.getByTestId('code-editor')
.getByRole('textbox')

View File

@ -33,6 +33,7 @@ test('table features should work', async ({ page }) => {
await expect(page.locator('text=Additional information')).toBeVisible()
await expect(page.locator('text=utm_source')).toBeVisible()
await expect(page.locator('text=utm_userid')).toBeVisible()
await expect(page.locator('text=utm_session')).toBeHidden()
})
await test.step('Resize columns', async () => {

View File

@ -177,7 +177,7 @@ test("can't add new members when limit is reached", async ({ page }) => {
await page.click('button >> text="Invite"')
await expect(
page.locator(
'text="Upgrade your plan to work with more team members, and unlock awesome power features 🚀"'
'text="Upgrade your plan to work with more team members, and unlock awesome power features"'
)
).toBeVisible()
await expect(page.locator('button >> text="Invite"')).toBeDisabled()

View File

@ -258,8 +258,21 @@
}
],
"variables": [
{ "id": "giiLFGw5xXBCHzvp1qAbdX", "name": "Name" },
{ "id": "3VFChNVSCXQ2rXv4DrJ8Ah", "name": "Email" },
{
"id": "giiLFGw5xXBCHzvp1qAbdX",
"name": "Name",
"isSessionVariable": true
},
{
"id": "3VFChNVSCXQ2rXv4DrJ8Ah",
"name": "Email",
"isSessionVariable": true
},
{
"id": "8Q8t9YCc3ieAEXSYkmnCxH",
"name": "utm_session",
"isSessionVariable": true
},
{ "id": "8Q8t9YCc3ieAEXSYkmnCxH", "name": "utm_source" },
{ "id": "ds54chWAyWC4zjkdWAm3Vc", "name": "utm_userid" }
],

View File

@ -126,6 +126,14 @@ This value block allows you to find the `Id` from `Ids` with the same index as `
/>
</Frame>
## Save in results
By default, new variables are not persisted in the [Results](../../../results) table. They are only stored for the current user chat session. Enabling this option will save the variable in the `Results` table.
## Execute on client
This option is useful when you want to execute the custom code on the client side. This is only necessary when you need access the user's browser information. So, if you need access to `window`, `document`, `navigator`, etc., you should enable this option.
## Get user's geo location
For this you can provide the following custom code:

View File

@ -40,7 +40,7 @@
"dotenv": "16.4.5",
"@faire/mjml-react": "3.3.0",
"@paralleldrive/cuid2": "2.2.1",
"@playwright/test": "1.36.0",
"@playwright/test": "1.43.1",
"@typebot.io/emails": "workspace:*",
"@typebot.io/env": "workspace:*",
"@typebot.io/forge": "workspace:*",
@ -55,7 +55,8 @@
"@types/papaparse": "5.3.7",
"@types/qs": "6.9.7",
"@types/react": "18.2.15",
"dotenv-cli": "7.2.1",
"dotenv-cli": "7.4.1",
"dotenv": "16.4.5",
"eslint": "8.44.0",
"eslint-config-custom": "workspace:*",
"google-auth-library": "8.9.0",

View File

@ -1,6 +1,8 @@
import { defineConfig, devices } from '@playwright/test'
import { resolve } from 'path'
process.env.SKIP_ENV_CHECK = 'true'
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('dotenv').config({ path: resolve(__dirname, '../../.env') })
export default defineConfig({
timeout: process.env.CI ? 50 * 1000 : 40 * 1000,

View File

@ -1,7 +1,7 @@
import prisma from '@typebot.io/lib/prisma'
import { Prisma } from '@typebot.io/prisma'
import { TypebotInSession } from '@typebot.io/schemas'
import { filterVariablesWithValues } from '@typebot.io/variables/filterVariablesWithValues'
import { filterNonSessionVariablesWithValues } from '@typebot.io/variables/filterVariablesWithValues'
type Props = {
resultId: string
@ -15,7 +15,9 @@ export const upsertResult = ({
hasStarted,
isCompleted,
}: Props): Prisma.PrismaPromise<any> => {
const variablesWithValue = filterVariablesWithValues(typebot.variables)
const variablesWithValue = filterNonSessionVariablesWithValues(
typebot.variables
)
return prisma.result.upsert({
where: { id: resultId },
update: {

View File

@ -149,7 +149,8 @@ const parseVariablesHeaders = (
if (
existingInputResultHeaders.some((existingInputResultHeader) =>
existingInputResultHeader.variableIds?.includes(variable.id)
)
) ||
variable.isSessionVariable
)
return existingHeaders

View File

@ -2,27 +2,27 @@ import { z } from '../../zod'
export const listVariableValue = z.array(z.string().nullable())
export const variableSchema = z.object({
const baseVariableSchema = z.object({
id: z.string(),
name: z.string(),
isSessionVariable: z.boolean().optional(),
})
export const variableSchema = baseVariableSchema.extend({
value: z.string().or(listVariableValue).nullish(),
})
/**
* Variable when retrieved from the database
*/
export const variableWithValueSchema = z.object({
id: z.string(),
name: z.string(),
export const variableWithValueSchema = baseVariableSchema.extend({
value: z.string().or(listVariableValue),
})
/**
* Variable when computed or retrieved from a block
*/
const VariableWithUnknowValueSchema = z.object({
id: z.string(),
name: z.string(),
const VariableWithUnknowValueSchema = baseVariableSchema.extend({
value: z.unknown(),
})

View File

@ -1,9 +1,9 @@
import { isDefined } from '@typebot.io/lib'
import { Variable, VariableWithValue } from '../schemas'
export const filterVariablesWithValues = (
export const filterNonSessionVariablesWithValues = (
variables: Variable[]
): VariableWithValue[] =>
variables.filter((variable) =>
isDefined(variable.value)
variables.filter(
(variable) => isDefined(variable.value) && !variable.isSessionVariable
) as VariableWithValue[]

69
pnpm-lock.yaml generated
View File

@ -283,8 +283,8 @@ importers:
specifier: 2.9.1
version: 2.9.1
'@playwright/test':
specifier: 1.41.2
version: 1.41.2
specifier: 1.43.1
version: 1.43.1
'@typebot.io/billing':
specifier: workspace:*
version: link:../../packages/billing
@ -354,9 +354,12 @@ importers:
'@types/tinycolor2':
specifier: 1.4.3
version: 1.4.3
dotenv:
specifier: 16.4.5
version: 16.4.5
dotenv-cli:
specifier: 7.2.1
version: 7.2.1
specifier: 7.4.1
version: 7.4.1
eslint:
specifier: 8.44.0
version: 8.44.0
@ -649,8 +652,8 @@ importers:
specifier: 2.2.1
version: 2.2.1
'@playwright/test':
specifier: 1.36.0
version: 1.36.0
specifier: 1.43.1
version: 1.43.1
'@typebot.io/emails':
specifier: workspace:*
version: link:../../packages/emails
@ -703,8 +706,8 @@ importers:
specifier: 16.4.5
version: 16.4.5
dotenv-cli:
specifier: 7.2.1
version: 7.2.1
specifier: 7.4.1
version: 7.4.1
eslint:
specifier: 8.44.0
version: 8.44.0
@ -7932,14 +7935,6 @@ packages:
fsevents: 2.3.2
dev: true
/@playwright/test@1.41.2:
resolution: {integrity: sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==}
engines: {node: '>=16'}
hasBin: true
dependencies:
playwright: 1.41.2
dev: true
/@playwright/test@1.42.1:
resolution: {integrity: sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==}
engines: {node: '>=16'}
@ -7948,6 +7943,14 @@ packages:
playwright: 1.42.1
dev: false
/@playwright/test@1.43.1:
resolution: {integrity: sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==}
engines: {node: '>=16'}
hasBin: true
dependencies:
playwright: 1.43.1
dev: true
/@popperjs/core@2.11.8:
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
dev: false
@ -13213,6 +13216,16 @@ packages:
minimist: 1.2.8
dev: true
/dotenv-cli@7.4.1:
resolution: {integrity: sha512-fE1aywjRrWGxV3miaiUr3d2zC/VAiuzEGghi+QzgIA9fEf/M5hLMaRSXb4IxbUAwGmaLi0IozdZddnVU96acag==}
hasBin: true
dependencies:
cross-spawn: 7.0.3
dotenv: 16.4.5
dotenv-expand: 10.0.0
minimist: 1.2.8
dev: true
/dotenv-expand@10.0.0:
resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==}
engines: {node: '>=12'}
@ -19262,26 +19275,16 @@ packages:
hasBin: true
dev: true
/playwright-core@1.41.2:
resolution: {integrity: sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==}
engines: {node: '>=16'}
hasBin: true
dev: true
/playwright-core@1.42.1:
resolution: {integrity: sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==}
engines: {node: '>=16'}
hasBin: true
dev: false
/playwright@1.41.2:
resolution: {integrity: sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==}
/playwright-core@1.43.1:
resolution: {integrity: sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==}
engines: {node: '>=16'}
hasBin: true
dependencies:
playwright-core: 1.41.2
optionalDependencies:
fsevents: 2.3.2
dev: true
/playwright@1.42.1:
@ -19294,6 +19297,16 @@ packages:
fsevents: 2.3.2
dev: false
/playwright@1.43.1:
resolution: {integrity: sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==}
engines: {node: '>=16'}
hasBin: true
dependencies:
playwright-core: 1.43.1
optionalDependencies:
fsevents: 2.3.2
dev: true
/pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}