feat(engine): ✨ Improve variables in executed codes
This commit is contained in:
@ -1,4 +1,4 @@
|
|||||||
import { Box, BoxProps } from '@chakra-ui/react'
|
import { Box, BoxProps, HStack } from '@chakra-ui/react'
|
||||||
import { EditorState, EditorView, basicSetup } from '@codemirror/basic-setup'
|
import { EditorState, EditorView, basicSetup } from '@codemirror/basic-setup'
|
||||||
import { json, jsonParseLinter } from '@codemirror/lang-json'
|
import { json, jsonParseLinter } from '@codemirror/lang-json'
|
||||||
import { css } from '@codemirror/lang-css'
|
import { css } from '@codemirror/lang-css'
|
||||||
@ -7,6 +7,8 @@ import { html } from '@codemirror/lang-html'
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useDebouncedCallback } from 'use-debounce'
|
import { useDebouncedCallback } from 'use-debounce'
|
||||||
import { linter } from '@codemirror/lint'
|
import { linter } from '@codemirror/lint'
|
||||||
|
import { VariablesButton } from './buttons/VariablesButton'
|
||||||
|
import { Variable } from 'models'
|
||||||
|
|
||||||
const linterExtension = linter(jsonParseLinter())
|
const linterExtension = linter(jsonParseLinter())
|
||||||
|
|
||||||
@ -15,12 +17,14 @@ type Props = {
|
|||||||
lang?: 'css' | 'json' | 'js' | 'html'
|
lang?: 'css' | 'json' | 'js' | 'html'
|
||||||
isReadOnly?: boolean
|
isReadOnly?: boolean
|
||||||
debounceTimeout?: number
|
debounceTimeout?: number
|
||||||
|
withVariableButton?: boolean
|
||||||
onChange?: (value: string) => void
|
onChange?: (value: string) => void
|
||||||
}
|
}
|
||||||
export const CodeEditor = ({
|
export const CodeEditor = ({
|
||||||
value,
|
value,
|
||||||
lang,
|
lang,
|
||||||
onChange,
|
onChange,
|
||||||
|
withVariableButton = true,
|
||||||
isReadOnly = false,
|
isReadOnly = false,
|
||||||
debounceTimeout = 1000,
|
debounceTimeout = 1000,
|
||||||
...props
|
...props
|
||||||
@ -28,6 +32,9 @@ export const CodeEditor = ({
|
|||||||
const editorContainer = useRef<HTMLDivElement | null>(null)
|
const editorContainer = useRef<HTMLDivElement | null>(null)
|
||||||
const editorView = useRef<EditorView | null>(null)
|
const editorView = useRef<EditorView | null>(null)
|
||||||
const [, setPlainTextValue] = useState(value)
|
const [, setPlainTextValue] = useState(value)
|
||||||
|
const [carretPosition, setCarretPosition] = useState<number>(0)
|
||||||
|
const isVariableButtonDisplayed = withVariableButton && !isReadOnly
|
||||||
|
|
||||||
const debounced = useDebouncedCallback(
|
const debounced = useDebouncedCallback(
|
||||||
(value) => {
|
(value) => {
|
||||||
setPlainTextValue(value)
|
setPlainTextValue(value)
|
||||||
@ -101,5 +108,35 @@ export const CodeEditor = ({
|
|||||||
return editor
|
return editor
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Box ref={editorContainer} data-testid="code-editor" {...props} />
|
const handleVariableSelected = (variable?: Pick<Variable, 'id' | 'name'>) => {
|
||||||
|
editorView.current?.focus()
|
||||||
|
const insert = `{{${variable?.name}}}`
|
||||||
|
editorView.current?.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: carretPosition,
|
||||||
|
insert,
|
||||||
|
},
|
||||||
|
selection: { anchor: carretPosition + insert.length },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyUp = () => {
|
||||||
|
if (!editorContainer.current) return
|
||||||
|
setCarretPosition(editorView.current?.state.selection.main.from ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack align="flex-end" spacing={0}>
|
||||||
|
<Box
|
||||||
|
w={isVariableButtonDisplayed ? 'calc(100% - 32px)' : '100%'}
|
||||||
|
ref={editorContainer}
|
||||||
|
data-testid="code-editor"
|
||||||
|
{...props}
|
||||||
|
onKeyUp={handleKeyUp}
|
||||||
|
/>
|
||||||
|
{isVariableButtonDisplayed && (
|
||||||
|
<VariablesButton onSelectVariable={handleVariableSelected} size="sm" />
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -144,7 +144,7 @@ export const BlockNode = ({ block, blockIndex }: Props) => {
|
|||||||
pointerEvents={isReadOnly || isStartBlock ? 'none' : 'auto'}
|
pointerEvents={isReadOnly || isStartBlock ? 'none' : 'auto'}
|
||||||
>
|
>
|
||||||
<EditablePreview
|
<EditablePreview
|
||||||
_hover={{ bgColor: 'gray.300' }}
|
_hover={{ bgColor: 'gray.200' }}
|
||||||
px="1"
|
px="1"
|
||||||
userSelect={'none'}
|
userSelect={'none'}
|
||||||
/>
|
/>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
import { FormLabel, HStack, Stack, Switch, Text } from '@chakra-ui/react'
|
||||||
|
import { CodeEditor } from 'components/shared/CodeEditor'
|
||||||
import { Textarea } from 'components/shared/Textbox'
|
import { Textarea } from 'components/shared/Textbox'
|
||||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||||
import { SetVariableOptions, Variable } from 'models'
|
import { SetVariableOptions, Variable } from 'models'
|
||||||
@ -14,6 +15,11 @@ export const SetVariableSettings = ({ options, onOptionsChange }: Props) => {
|
|||||||
onOptionsChange({ ...options, variableId: variable?.id })
|
onOptionsChange({ ...options, variableId: variable?.id })
|
||||||
const handleExpressionChange = (expressionToEvaluate: string) =>
|
const handleExpressionChange = (expressionToEvaluate: string) =>
|
||||||
onOptionsChange({ ...options, expressionToEvaluate })
|
onOptionsChange({ ...options, expressionToEvaluate })
|
||||||
|
const handleValueTypeChange = () =>
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
isCode: options.isCode ? !options.isCode : true,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
@ -28,14 +34,34 @@ export const SetVariableSettings = ({ options, onOptionsChange }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<FormLabel mb="0" htmlFor="expression">
|
<HStack justify="space-between">
|
||||||
Value / Expression:
|
<FormLabel mb="0" htmlFor="expression">
|
||||||
</FormLabel>
|
Value:
|
||||||
<Textarea
|
</FormLabel>
|
||||||
id="expression"
|
<HStack>
|
||||||
defaultValue={options.expressionToEvaluate ?? ''}
|
<Text fontSize="sm">Text</Text>
|
||||||
onChange={handleExpressionChange}
|
<Switch
|
||||||
/>
|
size="sm"
|
||||||
|
isChecked={options.isCode ?? false}
|
||||||
|
onChange={handleValueTypeChange}
|
||||||
|
/>
|
||||||
|
<Text fontSize="sm">Code</Text>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{options.isCode ?? false ? (
|
||||||
|
<CodeEditor
|
||||||
|
value={options.expressionToEvaluate ?? ''}
|
||||||
|
onChange={handleExpressionChange}
|
||||||
|
lang="js"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Textarea
|
||||||
|
id="expression"
|
||||||
|
defaultValue={options.expressionToEvaluate ?? ''}
|
||||||
|
onChange={handleExpressionChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
|
@ -23,7 +23,6 @@ export const BlocksDropdown = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const handleBlockSelect = (title: string) => {
|
const handleBlockSelect = (title: string) => {
|
||||||
console.log(title)
|
|
||||||
const id = blocks?.find((b) => b.title === title)?.id
|
const id = blocks?.find((b) => b.title === title)?.id
|
||||||
if (id) onBlockIdSelected(id)
|
if (id) onBlockIdSelected(id)
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
ComponentWithAs,
|
ComponentWithAs,
|
||||||
Flex,
|
|
||||||
HStack,
|
HStack,
|
||||||
IconButton,
|
|
||||||
InputProps,
|
InputProps,
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
TextareaProps,
|
TextareaProps,
|
||||||
Tooltip,
|
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { UserIcon } from 'assets/icons'
|
|
||||||
import { Variable } from 'models'
|
import { Variable } from 'models'
|
||||||
import React, { ChangeEvent, useEffect, useRef, useState } from 'react'
|
import React, { ChangeEvent, useEffect, useRef, useState } from 'react'
|
||||||
import { useDebouncedCallback } from 'use-debounce'
|
import { useDebouncedCallback } from 'use-debounce'
|
||||||
import { VariableSearchInput } from '../VariableSearchInput'
|
import { VariablesButton } from '../buttons/VariablesButton'
|
||||||
|
|
||||||
export type TextBoxProps = {
|
export type TextBoxProps = {
|
||||||
onChange: (value: string) => void
|
onChange: (value: string) => void
|
||||||
@ -120,27 +113,7 @@ export const TextBox = ({
|
|||||||
bgColor={'white'}
|
bgColor={'white'}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<Popover matchWidth isLazy>
|
<VariablesButton onSelectVariable={handleVariableSelected} />
|
||||||
<PopoverTrigger>
|
|
||||||
<Flex>
|
|
||||||
<Tooltip label="Insert a variable">
|
|
||||||
<IconButton
|
|
||||||
aria-label="Insert a variable"
|
|
||||||
icon={<UserIcon />}
|
|
||||||
pos="relative"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Flex>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent w="full">
|
|
||||||
<VariableSearchInput
|
|
||||||
onSelectVariable={handleVariableSelected}
|
|
||||||
placeholder="Search for a variable"
|
|
||||||
shadow="lg"
|
|
||||||
isDefaultOpen
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
46
apps/builder/components/shared/buttons/VariablesButton.tsx
Normal file
46
apps/builder/components/shared/buttons/VariablesButton.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
Flex,
|
||||||
|
Tooltip,
|
||||||
|
IconButton,
|
||||||
|
PopoverContent,
|
||||||
|
IconButtonProps,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { UserIcon } from 'assets/icons'
|
||||||
|
import { Variable } from 'models'
|
||||||
|
import React from 'react'
|
||||||
|
import { VariableSearchInput } from '../VariableSearchInput'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onSelectVariable: (
|
||||||
|
variable: Pick<Variable, 'name' | 'id'> | undefined
|
||||||
|
) => void
|
||||||
|
} & Omit<IconButtonProps, 'aria-label'>
|
||||||
|
|
||||||
|
export const VariablesButton = ({ onSelectVariable, ...props }: Props) => {
|
||||||
|
return (
|
||||||
|
<Popover matchWidth isLazy>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Flex>
|
||||||
|
<Tooltip label="Insert a variable">
|
||||||
|
<IconButton
|
||||||
|
aria-label={'Insert a variable'}
|
||||||
|
icon={<UserIcon />}
|
||||||
|
pos="relative"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent w="full">
|
||||||
|
<VariableSearchInput
|
||||||
|
onSelectVariable={onSelectVariable}
|
||||||
|
placeholder="Search for a variable"
|
||||||
|
shadow="lg"
|
||||||
|
isDefaultOpen
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
@ -98,7 +98,7 @@
|
|||||||
"blockId": "cl1267q1z000d2e6d949f2ge4",
|
"blockId": "cl1267q1z000d2e6d949f2ge4",
|
||||||
"options": {
|
"options": {
|
||||||
"name": "Store Name in DB",
|
"name": "Store Name in DB",
|
||||||
"content": "postMessage({from: \"typebot\", action: \"storeName\", content: \"{{Name}}\"}, \"*\")"
|
"content": "postMessage({from: \"typebot\", action: \"storeName\", content: {{Name}}}, \"*\")"
|
||||||
},
|
},
|
||||||
"outgoingEdgeId": "cl12bk56s000d2e69oll3nqxm"
|
"outgoingEdgeId": "cl12bk56s000d2e69oll3nqxm"
|
||||||
}
|
}
|
||||||
@ -239,7 +239,7 @@
|
|||||||
"blockId": "cl126jioj000u2e6dqssno3hv",
|
"blockId": "cl126jioj000u2e6dqssno3hv",
|
||||||
"options": {
|
"options": {
|
||||||
"name": "Store company in DB",
|
"name": "Store company in DB",
|
||||||
"content": "postMessage({from: \"typebot\", action: \"storeCompany\", content: \"{{Company}}\"}, \"*\")"
|
"content": "postMessage({from: \"typebot\", action: \"storeCompany\", content: {{Company}}}, \"*\")"
|
||||||
},
|
},
|
||||||
"outgoingEdgeId": "cl128ag8i00162e6dufv3tgo0"
|
"outgoingEdgeId": "cl128ag8i00162e6dufv3tgo0"
|
||||||
}
|
}
|
||||||
@ -335,7 +335,7 @@
|
|||||||
"blockId": "cl126krbp00102e6dnjelmfa1",
|
"blockId": "cl126krbp00102e6dnjelmfa1",
|
||||||
"options": {
|
"options": {
|
||||||
"name": "Store categories in DB",
|
"name": "Store categories in DB",
|
||||||
"content": "postMessage({from: \"typebot\", action: \"storeCategories\", content: \"{{Categories}}\"}, \"*\")"
|
"content": "postMessage({from: \"typebot\", action: \"storeCategories\", content: {{Categories}}}, \"*\")"
|
||||||
},
|
},
|
||||||
"outgoingEdgeId": "cl128azam00182e6dct61k7v5"
|
"outgoingEdgeId": "cl128azam00182e6dct61k7v5"
|
||||||
}
|
}
|
||||||
@ -458,7 +458,7 @@
|
|||||||
"blockId": "cl126pv6w001n2e6dp0qkvthu",
|
"blockId": "cl126pv6w001n2e6dp0qkvthu",
|
||||||
"options": {
|
"options": {
|
||||||
"name": "Store Other categories in DB",
|
"name": "Store Other categories in DB",
|
||||||
"content": "postMessage({from: \"typebot\", action: \"storeOtherCategories\", content: \"{{Other categories}}\"}, \"*\")"
|
"content": "postMessage({from: \"typebot\", action: \"storeOtherCategories\", content: {{Other categories}}}, \"*\")"
|
||||||
},
|
},
|
||||||
"outgoingEdgeId": "cl128c0fu001a2e6droq69g6z"
|
"outgoingEdgeId": "cl128c0fu001a2e6droq69g6z"
|
||||||
}
|
}
|
||||||
|
@ -247,9 +247,8 @@ const executeWebhook = async (
|
|||||||
if (!varMapping?.bodyPath || !varMapping.variableId) return newVariables
|
if (!varMapping?.bodyPath || !varMapping.variableId) return newVariables
|
||||||
const existingVariable = variables.find(byId(varMapping.variableId))
|
const existingVariable = variables.find(byId(varMapping.variableId))
|
||||||
if (!existingVariable) return newVariables
|
if (!existingVariable) return newVariables
|
||||||
const value = Function(
|
const func = Function('data', `return data.${varMapping?.bodyPath}`)
|
||||||
`return (${JSON.stringify(data)}).${varMapping?.bodyPath}`
|
const value = func(JSON.stringify(data))
|
||||||
)()
|
|
||||||
updateVariableValue(existingVariable?.id, value)
|
updateVariableValue(existingVariable?.id, value)
|
||||||
return [...newVariables, { ...existingVariable, value }]
|
return [...newVariables, { ...existingVariable, value }]
|
||||||
}, [])
|
}, [])
|
||||||
|
@ -63,9 +63,8 @@ const executeSetVariable = (
|
|||||||
): EdgeId | undefined => {
|
): EdgeId | undefined => {
|
||||||
if (!step.options?.variableId || !step.options.expressionToEvaluate)
|
if (!step.options?.variableId || !step.options.expressionToEvaluate)
|
||||||
return step.outgoingEdgeId
|
return step.outgoingEdgeId
|
||||||
const expression = step.options.expressionToEvaluate
|
const evaluatedExpression = evaluateExpression(variables)(
|
||||||
const evaluatedExpression = evaluateExpression(
|
step.options.expressionToEvaluate
|
||||||
parseVariables(variables)(expression)
|
|
||||||
)
|
)
|
||||||
const existingVariable = variables.find(byId(step.options.variableId))
|
const existingVariable = variables.find(byId(step.options.variableId))
|
||||||
if (!existingVariable) return step.outgoingEdgeId
|
if (!existingVariable) return step.outgoingEdgeId
|
||||||
@ -132,7 +131,11 @@ const executeCode = async (
|
|||||||
{ typebot: { variables } }: LogicContext
|
{ typebot: { variables } }: LogicContext
|
||||||
) => {
|
) => {
|
||||||
if (!step.options.content) return
|
if (!step.options.content) return
|
||||||
await Function(parseVariables(variables)(step.options.content))()
|
const func = Function(
|
||||||
|
...variables.map((v) => v.id),
|
||||||
|
parseVariables(variables, { fieldToParse: 'id' })(step.options.content)
|
||||||
|
)
|
||||||
|
await func(...variables.map((v) => v.value))
|
||||||
return step.outgoingEdgeId
|
return step.outgoingEdgeId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,22 +5,33 @@ export const stringContainsVariable = (str: string): boolean =>
|
|||||||
/\{\{(.*?)\}\}/g.test(str)
|
/\{\{(.*?)\}\}/g.test(str)
|
||||||
|
|
||||||
export const parseVariables =
|
export const parseVariables =
|
||||||
(variables: Variable[]) =>
|
(
|
||||||
(text?: string): string => {
|
variables: Variable[],
|
||||||
|
options: { fieldToParse: 'value' | 'id' } = { fieldToParse: 'value' }
|
||||||
|
) =>
|
||||||
|
(text: string | undefined): string => {
|
||||||
if (!text || text === '') return ''
|
if (!text || text === '') return ''
|
||||||
return text.replace(/\{\{(.*?)\}\}/g, (_, fullVariableString) => {
|
return text.replace(/\{\{(.*?)\}\}/g, (_, fullVariableString) => {
|
||||||
const matchedVarName = fullVariableString.replace(/{{|}}/g, '')
|
const matchedVarName = fullVariableString.replace(/{{|}}/g, '')
|
||||||
|
const variable = variables.find((v) => {
|
||||||
|
return matchedVarName === v.name && isDefined(v.value)
|
||||||
|
})
|
||||||
|
if (!variable) return ''
|
||||||
return (
|
return (
|
||||||
variables.find((v) => {
|
(options.fieldToParse === 'value' ? variable.value : variable.id) || ''
|
||||||
return matchedVarName === v.name && isDefined(v.value)
|
|
||||||
})?.value ?? ''
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const evaluateExpression = (str: string) => {
|
export const evaluateExpression = (variables: Variable[]) => (str: string) => {
|
||||||
try {
|
try {
|
||||||
const evaluatedResult = Function('return ' + str)()
|
const func = Function(
|
||||||
|
...variables.map((v) => v.id),
|
||||||
|
parseVariables(variables, { fieldToParse: 'id' })(
|
||||||
|
str.includes('return ') ? str : `return ${str}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const evaluatedResult = func(...variables.map((v) => v.value))
|
||||||
return isNotDefined(evaluatedResult) ? '' : evaluatedResult.toString()
|
return isNotDefined(evaluatedResult) ? '' : evaluatedResult.toString()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
|
@ -81,6 +81,7 @@ export type Comparison = {
|
|||||||
export type SetVariableOptions = {
|
export type SetVariableOptions = {
|
||||||
variableId?: string
|
variableId?: string
|
||||||
expressionToEvaluate?: string
|
expressionToEvaluate?: string
|
||||||
|
isCode?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RedirectOptions = {
|
export type RedirectOptions = {
|
||||||
|
Reference in New Issue
Block a user