2
0

Add predefined set variable values (#497)

Closes #234
This commit is contained in:
Baptiste Arnaud
2023-05-11 17:17:24 -04:00
committed by GitHub
parent 9abc50dce5
commit bde941613c
9 changed files with 222 additions and 85 deletions

View File

@ -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 />}

View File

@ -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}`
}
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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"/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 121 KiB

View File

@ -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
}
}
}

View File

@ -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(),
})