2
0

Add variables panel

Closes #398
This commit is contained in:
Baptiste Arnaud
2024-05-13 09:58:27 +02:00
parent 218f689269
commit 1afa25a015
11 changed files with 264 additions and 28 deletions

View File

@ -671,3 +671,10 @@ export const RepeatIcon = (props: IconProps) => (
<path d="M11 6h6a2 2 0 0 1 2 2v10" />
</Icon>
)
export const BracesIcon = (props: IconProps) => (
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
<path d="M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1" />
<path d="M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1" />
</Icon>
)

View File

@ -1,17 +1,18 @@
import {
Flex,
FlexProps,
HStack,
IconButton,
Menu,
MenuButton,
MenuItem,
MenuList,
StackProps,
useColorModeValue,
useDisclosure,
} from '@chakra-ui/react'
import assert from 'assert'
import {
BookIcon,
BracesIcon,
DownloadIcon,
MoreVerticalIcon,
SettingsIcon,
@ -23,14 +24,16 @@ import { parseDefaultPublicId } from '@/features/publish/helpers/parseDefaultPub
import { useTranslate } from '@tolgee/react'
import { useUser } from '@/features/account/hooks/useUser'
import { useRouter } from 'next/router'
import { RightPanel, useEditor } from '../providers/EditorProvider'
export const BoardMenuButton = (props: FlexProps) => {
export const BoardMenuButton = (props: StackProps) => {
const { query } = useRouter()
const { typebot, currentUserMode } = useTypebot()
const { user } = useUser()
const [isDownloading, setIsDownloading] = useState(false)
const { isOpen, onOpen, onClose } = useDisclosure()
const { t } = useTranslate()
const { setRightPanel } = useEditor()
useEffect(() => {
if (user && !user.graphNavigation && !query.isFirstBot) onOpen()
@ -57,11 +60,15 @@ export const BoardMenuButton = (props: FlexProps) => {
window.open('https://docs.typebot.io/editor/graph', '_blank')
return (
<Flex
bgColor={useColorModeValue('white', 'gray.900')}
rounded="md"
{...props}
>
<HStack rounded="md" spacing="4" {...props}>
<IconButton
icon={<BracesIcon />}
aria-label="Open variables drawer"
size="sm"
shadow="lg"
bgColor={useColorModeValue('white', undefined)}
onClick={() => setRightPanel(RightPanel.VARIABLES)}
/>
<Menu>
<MenuButton
as={IconButton}
@ -86,6 +93,6 @@ export const BoardMenuButton = (props: FlexProps) => {
</MenuList>
<EditorSettingsModal isOpen={isOpen} onClose={onClose} />
</Menu>
</Flex>
</HStack>
)
}

View File

@ -18,6 +18,7 @@ import { EventsCoordinatesProvider } from '@/features/graph/providers/EventsCoor
import { TypebotNotFoundPage } from './TypebotNotFoundPage'
import { SuspectedTypebotBanner } from './SuspectedTypebotBanner'
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { VariablesDrawer } from '@/features/preview/components/VariablesDrawer'
export const EditorPage = () => {
const { typebot, currentUserMode, is404 } = useTypebot()
@ -78,6 +79,14 @@ export const EditorPage = () => {
}
const RightPanel = () => {
const { rightPanel } = useEditor()
return rightPanel === RightPanelEnum.PREVIEW ? <PreviewDrawer /> : <></>
const { rightPanel, setRightPanel } = useEditor()
switch (rightPanel) {
case RightPanelEnum.PREVIEW:
return <PreviewDrawer />
case RightPanelEnum.VARIABLES:
return <VariablesDrawer onClose={() => setRightPanel(undefined)} />
case undefined:
return null
}
}

View File

@ -278,7 +278,8 @@ const RightElements = ({
<Flex pos="relative">
<ShareTypebotButton isLoading={isNotDefined(typebot)} />
</Flex>
{router.pathname.includes('/edit') && isNotDefined(rightPanel) && (
{router.pathname.includes('/edit') &&
rightPanel !== RightPanel.PREVIEW && (
<Button
colorScheme="gray"
onClick={handlePreviewClick}

View File

@ -9,6 +9,7 @@ import {
export enum RightPanel {
PREVIEW,
VARIABLES,
}
const editorContext = createContext<{

View File

@ -15,7 +15,7 @@ export const variablesAction = (setTypebot: SetTypebot): VariablesActions => ({
createVariable: (newVariable: Variable) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
typebot.variables.push(newVariable)
typebot.variables.unshift(newVariable)
})
),
updateVariable: (

View File

@ -0,0 +1,201 @@
import {
Fade,
Flex,
HStack,
useColorModeValue,
IconButton,
Popover,
PopoverTrigger,
PopoverBody,
PopoverContent,
Stack,
Editable,
EditablePreview,
EditableInput,
Heading,
Input,
CloseButton,
SlideFade,
} from '@chakra-ui/react'
import { useTypebot } from '../../editor/providers/TypebotProvider'
import { FormEvent, useState } from 'react'
import { headerHeight } from '../../editor/constants'
import { useDrag } from '@use-gesture/react'
import { ResizeHandle } from './ResizeHandle'
import { Variable } from '@typebot.io/schemas'
import {
CheckIcon,
MoreHorizontalIcon,
PlusIcon,
TrashIcon,
} from '@/components/icons'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { isNotEmpty } from '@typebot.io/lib'
import { createId } from '@paralleldrive/cuid2'
type Props = {
onClose: () => void
}
export const VariablesDrawer = ({ onClose }: Props) => {
const { typebot, createVariable, updateVariable, deleteVariable } =
useTypebot()
const [width, setWidth] = useState(500)
const [isResizeHandleVisible, setIsResizeHandleVisible] = useState(false)
const [searchValue, setSearchValue] = useState('')
const filteredVariables = typebot?.variables.filter((v) =>
isNotEmpty(searchValue)
? v.name.toLowerCase().includes(searchValue.toLowerCase())
: true
)
const [isVariableCreated, setIsVariableCreated] = useState(false)
const useResizeHandleDrag = useDrag(
(state) => {
setWidth(-state.offset[0])
},
{
from: () => [-width, 0],
}
)
const handleCreateSubmit = (e: FormEvent) => {
e.preventDefault()
setIsVariableCreated(true)
setTimeout(() => setIsVariableCreated(false), 500)
setSearchValue('')
createVariable({
id: createId(),
isSessionVariable: true,
name: searchValue,
})
}
return (
<Flex
pos="absolute"
right="0"
top={`0`}
h={`100%`}
bgColor={useColorModeValue('white', 'gray.900')}
borderLeftWidth={'1px'}
shadow="lg"
borderLeftRadius={'lg'}
onMouseOver={() => setIsResizeHandleVisible(true)}
onMouseLeave={() => setIsResizeHandleVisible(false)}
p="6"
zIndex={10}
style={{ width: `${width}px` }}
>
<Fade in={isResizeHandleVisible}>
<ResizeHandle
{...useResizeHandleDrag()}
pos="absolute"
left="-7.5px"
top={`calc(50% - ${headerHeight}px)`}
/>
</Fade>
<Stack w="full" spacing="4">
<CloseButton pos="absolute" right="1rem" top="1rem" onClick={onClose} />
<Heading fontSize="md">Variables</Heading>
<HStack as="form" onSubmit={handleCreateSubmit}>
<Input
width="full"
placeholder="Search or create..."
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
/>
<SlideFade
in={
isVariableCreated ||
(filteredVariables && filteredVariables.length === 0)
}
unmountOnExit
offsetY={0}
offsetX={10}
>
<IconButton
isDisabled={isVariableCreated}
icon={isVariableCreated ? <CheckIcon /> : <PlusIcon />}
aria-label="Create"
type="submit"
colorScheme={isVariableCreated ? 'green' : 'blue'}
flexShrink={0}
/>
</SlideFade>
</HStack>
<Stack overflowY="auto" py="1">
{filteredVariables?.map((variable) => (
<VariableItem
key={variable.id}
variable={variable}
onChange={(changes) => updateVariable(variable.id, changes)}
onDelete={() => deleteVariable(variable.id)}
/>
))}
</Stack>
</Stack>
</Flex>
)
}
const VariableItem = ({
variable,
onChange,
onDelete,
}: {
variable: Variable
onChange: (variable: Partial<Variable>) => void
onDelete: () => void
}) => (
<HStack justifyContent="space-between">
<Editable
defaultValue={variable.name}
onSubmit={(name) => onChange({ name })}
>
<EditablePreview
px="2"
noOfLines={1}
cursor="text"
_hover={{
bg: useColorModeValue('gray.100', 'gray.700'),
}}
/>
<EditableInput ml="1" pl="1" />
</Editable>
<HStack>
<Popover>
<PopoverTrigger>
<IconButton
icon={<MoreHorizontalIcon />}
aria-label={'Settings'}
size="sm"
/>
</PopoverTrigger>
<PopoverContent>
<PopoverBody>
<SwitchWithLabel
label="Save in results?"
moreInfoContent="Check this option if you want to save the variable value in the typebot Results table."
initialValue={!variable.isSessionVariable}
onCheckChange={() =>
onChange({
...variable,
isSessionVariable: !variable.isSessionVariable,
})
}
/>
</PopoverBody>
</PopoverContent>
<IconButton
icon={<TrashIcon />}
onClick={onDelete}
aria-label="Delete"
size="sm"
/>
</Popover>
</HStack>
</HStack>
)

View File

@ -63,7 +63,7 @@ You can customize how your bot behaves on WhatsApp in the `Configure integration
## Collect position
You can ask for the user's location with a basic [Text input block](../editor/blocks/inputs/text). It will be saved as a variable with the latitude and longitude with the following format: `<LAT>, <LONG>`.
You can ask for the user's location with a basic [Text input block](../../editor/blocks/inputs/text). It will be saved as a variable with the latitude and longitude with the following format: `<LAT>, <LONG>`.
<Tabs>
<Tab title="Flow">

View File

@ -35,6 +35,16 @@ Likewise for last item:
`{{={{My variable}}.at(-1)=}}`
## Variables panel
You can access the variables panel by clicking on the "Variables" button in the top right corner of the editor:
<Frame>
<img src="/images/variables/variablesPanel.jpg" alt="Variables panel" />
</Frame>
In this panel you can see all the variables declared in your bot. There, you can easily rename, edit, delete your variables.
## Advanced concepts
Here is a quick video that showcases advanced concepts about variables:

View File

@ -12,4 +12,4 @@ Once you have saved the UTM values into variables like `utm_source` and `utm_val
https://redirect-site.com?utm_source={{utm_source}}&utm_value={{utm_value}}
```
<LoomVideo id="9b6cb65aff0a485e9e021b42310b207c" />
<LoomVideo id="d298d5f4e1d04190b9768ffb6665bef8" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB