@ -35,7 +35,7 @@ type Item =
|
||||
type Props<T extends Item> = {
|
||||
isPopoverMatchingInputWidth?: boolean
|
||||
selectedItem?: string
|
||||
items: T[]
|
||||
items: readonly T[]
|
||||
placeholder?: string
|
||||
onSelect?: (value: string | undefined, item?: T) => void
|
||||
}
|
||||
@ -190,11 +190,11 @@ export const Select = <T extends Item>({
|
||||
/>
|
||||
|
||||
<InputRightElement
|
||||
width={selectedItem ? '5rem' : undefined}
|
||||
width={selectedItem && isOpen ? '5rem' : undefined}
|
||||
pointerEvents="none"
|
||||
>
|
||||
<HStack>
|
||||
{selectedItem && (
|
||||
{selectedItem && isOpen && (
|
||||
<IconButton
|
||||
onClick={clearSelection}
|
||||
icon={<CloseIcon />}
|
||||
|
@ -1,18 +1,49 @@
|
||||
import { Text } from '@chakra-ui/react'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { SetVariableBlock } from '@typebot.io/schemas'
|
||||
import { byId } from '@typebot.io/lib'
|
||||
import { SetVariableBlock, Variable } from '@typebot.io/schemas'
|
||||
import { byId, isEmpty } from '@typebot.io/lib'
|
||||
|
||||
export const SetVariableContent = ({ block }: { block: SetVariableBlock }) => {
|
||||
const { typebot } = useTypebot()
|
||||
const variableName =
|
||||
typebot?.variables.find(byId(block.options.variableId))?.name ?? ''
|
||||
const expression = block.options.expressionToEvaluate ?? ''
|
||||
return (
|
||||
<Text color={'gray.500'} noOfLines={2}>
|
||||
{variableName === '' && expression === ''
|
||||
<Text color={'gray.500'} noOfLines={4}>
|
||||
{variableName === '' && isEmpty(block.options.expressionToEvaluate)
|
||||
? 'Click to edit...'
|
||||
: `${variableName} ${expression ? `= ${expression}` : ``}`}
|
||||
: getExpression(typebot?.variables ?? [])(block.options)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
const getExpression =
|
||||
(variables: Variable[]) =>
|
||||
(options: SetVariableBlock['options']): string | null => {
|
||||
const variableName = variables.find(byId(options.variableId))?.name ?? ''
|
||||
switch (options.type) {
|
||||
case 'Custom':
|
||||
case undefined:
|
||||
return `${variableName} = ${options.expressionToEvaluate}`
|
||||
case 'Map item with same index': {
|
||||
const baseItemVariable = variables.find(
|
||||
byId(options.mapListItemParams?.baseItemVariableId)
|
||||
)
|
||||
const baseListVariable = variables.find(
|
||||
byId(options.mapListItemParams?.baseListVariableId)
|
||||
)
|
||||
const targetListVariable = variables.find(
|
||||
byId(options.mapListItemParams?.targetListVariableId)
|
||||
)
|
||||
return `${variableName} = item in ${targetListVariable?.name} with same index as ${baseItemVariable?.name} in ${baseListVariable?.name}`
|
||||
}
|
||||
case 'Empty':
|
||||
return `Reset ${variableName}`
|
||||
case 'Random ID':
|
||||
case 'Today':
|
||||
case 'Tomorrow':
|
||||
case 'User ID':
|
||||
case 'Yesterday': {
|
||||
return `${variableName} = ${options.type}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { FormLabel, HStack, Stack, Switch, Text } from '@chakra-ui/react'
|
||||
import { FormLabel, Stack, Text } from '@chakra-ui/react'
|
||||
import { CodeEditor } from '@/components/inputs/CodeEditor'
|
||||
import { SetVariableOptions, Variable } from '@typebot.io/schemas'
|
||||
import { SetVariableOptions, Variable, valueTypes } from '@typebot.io/schemas'
|
||||
import React from 'react'
|
||||
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
|
||||
import { Textarea } from '@/components/inputs'
|
||||
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
|
||||
import { Select } from '@/components/inputs/Select'
|
||||
|
||||
type Props = {
|
||||
options: SetVariableOptions
|
||||
@ -15,19 +15,10 @@ export const SetVariableSettings = ({ options, onOptionsChange }: Props) => {
|
||||
const updateVariableId = (variable?: Variable) =>
|
||||
onOptionsChange({ ...options, variableId: variable?.id })
|
||||
|
||||
const updateExpression = (expressionToEvaluate: string) =>
|
||||
onOptionsChange({ ...options, expressionToEvaluate })
|
||||
|
||||
const updateExpressionType = () =>
|
||||
const updateValueType = (type?: string) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
isCode: options.isCode ? !options.isCode : true,
|
||||
})
|
||||
|
||||
const updateClientExecution = (isExecutedOnClient: boolean) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
isExecutedOnClient,
|
||||
type: type as SetVariableOptions['type'],
|
||||
})
|
||||
|
||||
return (
|
||||
@ -42,42 +33,110 @@ export const SetVariableSettings = ({ options, onOptionsChange }: Props) => {
|
||||
id="variable-search"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<HStack justify="space-between">
|
||||
<FormLabel mb="0" htmlFor="expression">
|
||||
Value:
|
||||
</FormLabel>
|
||||
<HStack>
|
||||
<Text fontSize="sm">Text</Text>
|
||||
<Switch
|
||||
size="sm"
|
||||
isChecked={options.isCode ?? false}
|
||||
onChange={updateExpressionType}
|
||||
/>
|
||||
<Text fontSize="sm">Code</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
{options.isCode ?? false ? (
|
||||
<Stack>
|
||||
<Text mb="0" fontWeight="medium">
|
||||
Value:
|
||||
</Text>
|
||||
<Select
|
||||
selectedItem={options.type ?? 'Custom'}
|
||||
items={valueTypes}
|
||||
onSelect={updateValueType}
|
||||
/>
|
||||
<SetVariableValue options={options} onOptionsChange={onOptionsChange} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const SetVariableValue = ({
|
||||
options,
|
||||
onOptionsChange,
|
||||
}: {
|
||||
options: SetVariableOptions
|
||||
onOptionsChange: (options: SetVariableOptions) => void
|
||||
}): JSX.Element | null => {
|
||||
const updateExpression = (expressionToEvaluate: string) =>
|
||||
onOptionsChange({ ...options, expressionToEvaluate })
|
||||
|
||||
const updateClientExecution = (isExecutedOnClient: boolean) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
isExecutedOnClient,
|
||||
})
|
||||
|
||||
const updateItemVariableId = (variable?: Variable) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
mapListItemParams: {
|
||||
...options.mapListItemParams,
|
||||
baseItemVariableId: variable?.id,
|
||||
},
|
||||
})
|
||||
|
||||
const updateBaseListVariableId = (variable?: Variable) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
mapListItemParams: {
|
||||
...options.mapListItemParams,
|
||||
baseListVariableId: variable?.id,
|
||||
},
|
||||
})
|
||||
|
||||
const updateTargetListVariableId = (variable?: Variable) =>
|
||||
onOptionsChange({
|
||||
...options,
|
||||
mapListItemParams: {
|
||||
...options.mapListItemParams,
|
||||
targetListVariableId: variable?.id,
|
||||
},
|
||||
})
|
||||
|
||||
switch (options.type) {
|
||||
case 'Custom':
|
||||
case undefined:
|
||||
return (
|
||||
<>
|
||||
<CodeEditor
|
||||
defaultValue={options.expressionToEvaluate ?? ''}
|
||||
onChange={updateExpression}
|
||||
lang="javascript"
|
||||
/>
|
||||
) : (
|
||||
<Textarea
|
||||
id="expression"
|
||||
defaultValue={options.expressionToEvaluate ?? ''}
|
||||
onChange={updateExpression}
|
||||
<SwitchWithLabel
|
||||
label="Execute on client?"
|
||||
moreInfoContent="Check this if you need access to client-only variables like `window` or `document`."
|
||||
initialValue={options.isExecutedOnClient ?? false}
|
||||
onCheckChange={updateClientExecution}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
<SwitchWithLabel
|
||||
label="Execute on client?"
|
||||
moreInfoContent="Check this if you need access to client-only variables like `window` or `document`."
|
||||
initialValue={options.isExecutedOnClient ?? false}
|
||||
onCheckChange={updateClientExecution}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
</>
|
||||
)
|
||||
case 'Map item with same index': {
|
||||
return (
|
||||
<Stack p="2" rounded="md" borderWidth={1}>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options.mapListItemParams?.baseItemVariableId}
|
||||
onSelectVariable={updateItemVariableId}
|
||||
placeholder="Base item"
|
||||
/>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options.mapListItemParams?.baseListVariableId}
|
||||
onSelectVariable={updateBaseListVariableId}
|
||||
placeholder="Base list"
|
||||
/>
|
||||
<VariableSearchInput
|
||||
initialVariableId={options.mapListItemParams?.targetListVariableId}
|
||||
onSelectVariable={updateTargetListVariableId}
|
||||
placeholder="Target list"
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
case 'Random ID':
|
||||
case 'Today':
|
||||
case 'Tomorrow':
|
||||
case 'User ID':
|
||||
case 'Yesterday':
|
||||
case 'Empty':
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,10 @@ test.describe('Set variable block', () => {
|
||||
await page.click('text=Click to edit... >> nth = 0')
|
||||
await page.fill('input[placeholder="Select a variable"] >> nth=-1', 'Total')
|
||||
await page.getByRole('menuitem', { name: 'Create Total' }).click()
|
||||
await page.fill('textarea', '1000 * {{Num}}')
|
||||
await page
|
||||
.getByTestId('code-editor')
|
||||
.getByRole('textbox')
|
||||
.fill('1000 * {{Num}}')
|
||||
|
||||
await page.click('text=Click to edit...', { force: true })
|
||||
await page.fill(
|
||||
@ -30,7 +33,10 @@ test.describe('Set variable block', () => {
|
||||
'Custom var'
|
||||
)
|
||||
await page.getByRole('menuitem', { name: 'Create Custom var' }).click()
|
||||
await page.fill('textarea', 'Custom value')
|
||||
await page
|
||||
.getByTestId('code-editor')
|
||||
.getByRole('textbox')
|
||||
.fill('Custom value')
|
||||
|
||||
await page.click('text=Click to edit...', { force: true })
|
||||
await page.fill(
|
||||
@ -38,7 +44,10 @@ test.describe('Set variable block', () => {
|
||||
'Addition'
|
||||
)
|
||||
await page.getByRole('menuitem', { name: 'Create Addition' }).click()
|
||||
await page.fill('textarea', '1000 + {{Total}}')
|
||||
await page
|
||||
.getByTestId('code-editor')
|
||||
.getByRole('textbox')
|
||||
.fill('1000 + {{Total}}')
|
||||
|
||||
await page.click('text=Preview')
|
||||
await page
|
||||
|
@ -8,9 +8,11 @@ The "Set variable" block allows you to set a particular value to a variable.
|
||||
|
||||
<img src="/img/blocks/logic/set-variable.png" width="600" alt="Set variable"/>
|
||||
|
||||
This value can be any kind of plain text but also **Javascript code**.
|
||||
## Custom
|
||||
|
||||
## Expressions with existing variables
|
||||
You can set your variable with any value with `Custom`. It can be any kind of plain text but also **Javascript code**.
|
||||
|
||||
### Expressions with existing variables
|
||||
|
||||
It means you can apply operations on existing variables.
|
||||
|
||||
@ -54,9 +56,7 @@ Transform existing variable to upper case or lower case:
|
||||
{{Name}}.toLowerCase()
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
The code value should be written Javascript. It will read the returned value of the code and set it to your variable.
|
||||
This can also be Javascript code. It will read the returned value of the code and set it to your variable.
|
||||
|
||||
```js
|
||||
const name = 'John' + 'Smith'
|
||||
@ -88,26 +88,12 @@ For example,
|
||||
|
||||
:::
|
||||
|
||||
## Current Date
|
||||
## Map
|
||||
|
||||
You can create a `Submitted at` (or any other name) variable using this code:
|
||||
This is a convenient value block that allows you to easily get an item from a list that has the same index as an item from another list.
|
||||
|
||||
```js
|
||||
new Date().toISOString()
|
||||
```
|
||||
When you are pulling data from another service, sometimes, you will have 2 lists: `Labels` and `Ids`. Labels are the data displayed to the user and Ids are the data used for other requests to that external service.
|
||||
|
||||
It will set the variable to the current date and time.
|
||||
This value block allows you to find the `Id` from `Ids` with the same index as `Label` in `Labels`
|
||||
|
||||
## Random ID
|
||||
|
||||
Or a random ID:
|
||||
|
||||
```js
|
||||
Math.round(Math.random() * 1000000)
|
||||
```
|
||||
|
||||
:::note
|
||||
Keep in mind that the code is executed on the server. So you don't have access to browser variables such as `window` or `document`.
|
||||
:::
|
||||
|
||||
The code can also be multi-line. The Set variable block will get the value following the `return` statement.
|
||||
<img src="/img/blocks/logic/set-variable-map-item.png" width="600" alt="Set variable map item with same index"/>
|
||||
|
BIN
apps/docs/static/img/blocks/logic/set-variable-map-item.png
vendored
Normal file
BIN
apps/docs/static/img/blocks/logic/set-variable-map-item.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 174 KiB |
BIN
apps/docs/static/img/blocks/logic/set-variable.png
vendored
BIN
apps/docs/static/img/blocks/logic/set-variable.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 288 KiB After Width: | Height: | Size: 121 KiB |
@ -33,10 +33,11 @@ export const executeSetVariable = async (
|
||||
],
|
||||
}
|
||||
}
|
||||
const evaluatedExpression = block.options.expressionToEvaluate
|
||||
? evaluateSetVariableExpression(variables)(
|
||||
block.options.expressionToEvaluate
|
||||
)
|
||||
const expressionToEvaluate = getExpressionToEvaluate(state.result.id)(
|
||||
block.options
|
||||
)
|
||||
const evaluatedExpression = expressionToEvaluate
|
||||
? evaluateSetVariableExpression(variables)(expressionToEvaluate)
|
||||
: undefined
|
||||
const existingVariable = variables.find(byId(block.options.variableId))
|
||||
if (!existingVariable) return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
@ -67,3 +68,35 @@ const evaluateSetVariableExpression =
|
||||
return parseVariables(variables)(str)
|
||||
}
|
||||
}
|
||||
|
||||
const getExpressionToEvaluate =
|
||||
(resultId: string | undefined) =>
|
||||
(options: SetVariableBlock['options']): string | null => {
|
||||
switch (options.type) {
|
||||
case 'Today':
|
||||
return 'new Date().toISOString()'
|
||||
case 'Tomorrow': {
|
||||
return 'new Date(Date.now() + 86400000).toISOString()'
|
||||
}
|
||||
case 'Yesterday': {
|
||||
return 'new Date(Date.now() - 86400000).toISOString()'
|
||||
}
|
||||
case 'Random ID': {
|
||||
return 'Math.random().toString(36).substring(2, 15)'
|
||||
}
|
||||
case 'User ID': {
|
||||
return resultId ?? 'Math.random().toString(36).substring(2, 15)'
|
||||
}
|
||||
case 'Map item with same index': {
|
||||
return `const itemIndex = ${options.mapListItemParams?.baseListVariableId}.indexOf(${options.mapListItemParams?.baseItemVariableId})
|
||||
return ${options.mapListItemParams?.targetListVariableId}.at(itemIndex)`
|
||||
}
|
||||
case 'Empty': {
|
||||
return null
|
||||
}
|
||||
case 'Custom':
|
||||
case undefined: {
|
||||
return options.expressionToEvaluate ?? null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,10 +2,29 @@ import { z } from 'zod'
|
||||
import { blockBaseSchema } from '../baseSchemas'
|
||||
import { LogicBlockType } from './enums'
|
||||
|
||||
export const valueTypes = [
|
||||
'Custom',
|
||||
'Empty',
|
||||
'User ID',
|
||||
'Today',
|
||||
'Yesterday',
|
||||
'Tomorrow',
|
||||
'Random ID',
|
||||
'Map item with same index',
|
||||
] as const
|
||||
|
||||
export const setVariableOptionsSchema = z.object({
|
||||
variableId: z.string().optional(),
|
||||
expressionToEvaluate: z.string().optional(),
|
||||
isCode: z.boolean().optional(),
|
||||
type: z.enum(valueTypes).optional(),
|
||||
mapListItemParams: z
|
||||
.object({
|
||||
baseItemVariableId: z.string().optional(),
|
||||
baseListVariableId: z.string().optional(),
|
||||
targetListVariableId: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
isExecutedOnClient: z.boolean().optional(),
|
||||
})
|
||||
|
||||
|
Reference in New Issue
Block a user