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> = { type Props<T extends Item> = {
isPopoverMatchingInputWidth?: boolean isPopoverMatchingInputWidth?: boolean
selectedItem?: string selectedItem?: string
items: T[] items: readonly T[]
placeholder?: string placeholder?: string
onSelect?: (value: string | undefined, item?: T) => void onSelect?: (value: string | undefined, item?: T) => void
} }
@ -190,11 +190,11 @@ export const Select = <T extends Item>({
/> />
<InputRightElement <InputRightElement
width={selectedItem ? '5rem' : undefined} width={selectedItem && isOpen ? '5rem' : undefined}
pointerEvents="none" pointerEvents="none"
> >
<HStack> <HStack>
{selectedItem && ( {selectedItem && isOpen && (
<IconButton <IconButton
onClick={clearSelection} onClick={clearSelection}
icon={<CloseIcon />} icon={<CloseIcon />}

View File

@ -1,18 +1,49 @@
import { Text } from '@chakra-ui/react' import { Text } from '@chakra-ui/react'
import { useTypebot } from '@/features/editor/providers/TypebotProvider' import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { SetVariableBlock } from '@typebot.io/schemas' import { SetVariableBlock, Variable } from '@typebot.io/schemas'
import { byId } from '@typebot.io/lib' import { byId, isEmpty } from '@typebot.io/lib'
export const SetVariableContent = ({ block }: { block: SetVariableBlock }) => { export const SetVariableContent = ({ block }: { block: SetVariableBlock }) => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
const variableName = const variableName =
typebot?.variables.find(byId(block.options.variableId))?.name ?? '' typebot?.variables.find(byId(block.options.variableId))?.name ?? ''
const expression = block.options.expressionToEvaluate ?? ''
return ( return (
<Text color={'gray.500'} noOfLines={2}> <Text color={'gray.500'} noOfLines={4}>
{variableName === '' && expression === '' {variableName === '' && isEmpty(block.options.expressionToEvaluate)
? 'Click to edit...' ? 'Click to edit...'
: `${variableName} ${expression ? `= ${expression}` : ``}`} : getExpression(typebot?.variables ?? [])(block.options)}
</Text> </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 { 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 React from 'react'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { Textarea } from '@/components/inputs'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { Select } from '@/components/inputs/Select'
type Props = { type Props = {
options: SetVariableOptions options: SetVariableOptions
@ -15,19 +15,10 @@ export const SetVariableSettings = ({ options, onOptionsChange }: Props) => {
const updateVariableId = (variable?: Variable) => const updateVariableId = (variable?: Variable) =>
onOptionsChange({ ...options, variableId: variable?.id }) onOptionsChange({ ...options, variableId: variable?.id })
const updateExpression = (expressionToEvaluate: string) => const updateValueType = (type?: string) =>
onOptionsChange({ ...options, expressionToEvaluate })
const updateExpressionType = () =>
onOptionsChange({ onOptionsChange({
...options, ...options,
isCode: options.isCode ? !options.isCode : true, type: type as SetVariableOptions['type'],
})
const updateClientExecution = (isExecutedOnClient: boolean) =>
onOptionsChange({
...options,
isExecutedOnClient,
}) })
return ( return (
@ -42,42 +33,110 @@ export const SetVariableSettings = ({ options, onOptionsChange }: Props) => {
id="variable-search" id="variable-search"
/> />
</Stack> </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 <CodeEditor
defaultValue={options.expressionToEvaluate ?? ''} defaultValue={options.expressionToEvaluate ?? ''}
onChange={updateExpression} onChange={updateExpression}
lang="javascript" lang="javascript"
/> />
) : ( <SwitchWithLabel
<Textarea label="Execute on client?"
id="expression" moreInfoContent="Check this if you need access to client-only variables like `window` or `document`."
defaultValue={options.expressionToEvaluate ?? ''} initialValue={options.isExecutedOnClient ?? false}
onChange={updateExpression} onCheckChange={updateClientExecution}
/> />
)} </>
</Stack> )
<SwitchWithLabel case 'Map item with same index': {
label="Execute on client?" return (
moreInfoContent="Check this if you need access to client-only variables like `window` or `document`." <Stack p="2" rounded="md" borderWidth={1}>
initialValue={options.isExecutedOnClient ?? false} <VariableSearchInput
onCheckChange={updateClientExecution} initialVariableId={options.mapListItemParams?.baseItemVariableId}
/> onSelectVariable={updateItemVariableId}
</Stack> 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.click('text=Click to edit... >> nth = 0')
await page.fill('input[placeholder="Select a variable"] >> nth=-1', 'Total') await page.fill('input[placeholder="Select a variable"] >> nth=-1', 'Total')
await page.getByRole('menuitem', { name: 'Create Total' }).click() 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.click('text=Click to edit...', { force: true })
await page.fill( await page.fill(
@ -30,7 +33,10 @@ test.describe('Set variable block', () => {
'Custom var' 'Custom var'
) )
await page.getByRole('menuitem', { name: 'Create Custom var' }).click() 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.click('text=Click to edit...', { force: true })
await page.fill( await page.fill(
@ -38,7 +44,10 @@ test.describe('Set variable block', () => {
'Addition' 'Addition'
) )
await page.getByRole('menuitem', { name: 'Create Addition' }).click() 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.click('text=Preview')
await page 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"/> <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. It means you can apply operations on existing variables.
@ -54,9 +56,7 @@ Transform existing variable to upper case or lower case:
{{Name}}.toLowerCase() {{Name}}.toLowerCase()
``` ```
## Code This can also be Javascript code. It will read the returned value of the code and set it to your variable.
The code value should be written Javascript. It will read the returned value of the code and set it to your variable.
```js ```js
const name = 'John' + 'Smith' 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 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.
new Date().toISOString()
```
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 <img src="/img/blocks/logic/set-variable-map-item.png" width="600" alt="Set variable map item with same index"/>
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.

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 const expressionToEvaluate = getExpressionToEvaluate(state.result.id)(
? evaluateSetVariableExpression(variables)( block.options
block.options.expressionToEvaluate )
) const evaluatedExpression = expressionToEvaluate
? evaluateSetVariableExpression(variables)(expressionToEvaluate)
: undefined : undefined
const existingVariable = variables.find(byId(block.options.variableId)) const existingVariable = variables.find(byId(block.options.variableId))
if (!existingVariable) return { outgoingEdgeId: block.outgoingEdgeId } if (!existingVariable) return { outgoingEdgeId: block.outgoingEdgeId }
@ -67,3 +68,35 @@ const evaluateSetVariableExpression =
return parseVariables(variables)(str) 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 { blockBaseSchema } from '../baseSchemas'
import { LogicBlockType } from './enums' 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({ export const setVariableOptionsSchema = z.object({
variableId: z.string().optional(), variableId: z.string().optional(),
expressionToEvaluate: z.string().optional(), expressionToEvaluate: z.string().optional(),
isCode: z.boolean().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(), isExecutedOnClient: z.boolean().optional(),
}) })