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

View File

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

View File

@ -278,20 +278,21 @@ const RightElements = ({
<Flex pos="relative"> <Flex pos="relative">
<ShareTypebotButton isLoading={isNotDefined(typebot)} /> <ShareTypebotButton isLoading={isNotDefined(typebot)} />
</Flex> </Flex>
{router.pathname.includes('/edit') && isNotDefined(rightPanel) && ( {router.pathname.includes('/edit') &&
<Button rightPanel !== RightPanel.PREVIEW && (
colorScheme="gray" <Button
onClick={handlePreviewClick} colorScheme="gray"
isLoading={isNotDefined(typebot) || isSavingLoading} onClick={handlePreviewClick}
leftIcon={<PlayIcon />} isLoading={isNotDefined(typebot) || isSavingLoading}
size="sm" leftIcon={<PlayIcon />}
iconSpacing={{ base: 0, xl: 2 }} size="sm"
> iconSpacing={{ base: 0, xl: 2 }}
<chakra.span display={{ base: 'none', xl: 'inline' }}> >
{t('editor.header.previewButton.label')} <chakra.span display={{ base: 'none', xl: 'inline' }}>
</chakra.span> {t('editor.header.previewButton.label')}
</Button> </chakra.span>
)} </Button>
)}
{currentUserMode === 'guest' && ( {currentUserMode === 'guest' && (
<Button <Button
as={Link} as={Link}

View File

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

View File

@ -15,7 +15,7 @@ export const variablesAction = (setTypebot: SetTypebot): VariablesActions => ({
createVariable: (newVariable: Variable) => createVariable: (newVariable: Variable) =>
setTypebot((typebot) => setTypebot((typebot) =>
produce(typebot, (typebot) => { produce(typebot, (typebot) => {
typebot.variables.push(newVariable) typebot.variables.unshift(newVariable)
}) })
), ),
updateVariable: ( 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 ## 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> <Tabs>
<Tab title="Flow"> <Tab title="Flow">

View File

@ -35,6 +35,16 @@ Likewise for last item:
`{{={{My variable}}.at(-1)=}}` `{{={{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 ## Advanced concepts
Here is a quick video that showcases advanced concepts about variables: 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}} 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