2
0

feat(editor): Add unlock/lock sidebar

This commit is contained in:
Baptiste Arnaud
2022-01-29 11:22:22 +01:00
parent 02bd2b94ba
commit 1c5bd06657
18 changed files with 249 additions and 170 deletions

View File

@ -297,3 +297,17 @@ export const GripIcon = (props: IconProps) => (
<circle cx="5" cy="15" r="1"></circle>
</Icon>
)
export const LockedIcon = (props: IconProps) => (
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</Icon>
)
export const UnlockedIcon = (props: IconProps) => (
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 9.9-1"></path>
</Icon>
)

View File

@ -2,7 +2,7 @@ import { Flex } from '@chakra-ui/react'
import React from 'react'
import Graph from './graph/Graph'
import { StepDndContext } from 'contexts/StepDndContext'
import { StepTypesList } from './StepTypesList'
import { StepsSideBar } from './StepsSideBar'
import { PreviewDrawer } from './preview/PreviewDrawer'
import { RightPanel, useEditor } from 'contexts/EditorContext'
import { GraphProvider } from 'contexts/GraphContext'
@ -13,7 +13,7 @@ export const Board = () => {
return (
<Flex flex="1" pos="relative" bgColor="gray.50" h="full">
<StepDndContext>
<StepTypesList />
<StepsSideBar />
<GraphProvider>
<Graph flex="1" />
<BoardMenuButton pos="absolute" right="40px" top="20px" />

View File

@ -1,137 +0,0 @@
import {
Stack,
Input,
Text,
SimpleGrid,
useEventListener,
Portal,
} from '@chakra-ui/react'
import {
BubbleStepType,
DraggableStepType,
InputStepType,
IntegrationStepType,
LogicStepType,
} from 'models'
import { useStepDnd } from 'contexts/StepDndContext'
import React, { useState } from 'react'
import { StepCard, StepCardOverlay } from './StepCard'
export const StepTypesList = () => {
const { setDraggedStepType, draggedStepType } = useStepDnd()
const [position, setPosition] = useState({
x: 0,
y: 0,
})
const [relativeCoordinates, setRelativeCoordinates] = useState({ x: 0, y: 0 })
const handleMouseMove = (event: MouseEvent) => {
if (!draggedStepType) return
const { clientX, clientY } = event
setPosition({
...position,
x: clientX - relativeCoordinates.x,
y: clientY - relativeCoordinates.y,
})
}
useEventListener('mousemove', handleMouseMove)
const handleMouseDown = (e: React.MouseEvent, type: DraggableStepType) => {
const element = e.currentTarget as HTMLDivElement
const rect = element.getBoundingClientRect()
setPosition({ x: rect.left, y: rect.top })
const x = e.clientX - rect.left
const y = e.clientY - rect.top
setRelativeCoordinates({ x, y })
setDraggedStepType(type)
}
const handleMouseUp = () => {
if (!draggedStepType) return
setDraggedStepType(undefined)
setPosition({
x: 0,
y: 0,
})
}
useEventListener('mouseup', handleMouseUp)
return (
<Stack
w="320px"
pos="absolute"
left="10px"
top="10px"
h="calc(100vh - 80px)"
rounded="lg"
shadow="xl"
borderWidth="1px"
zIndex="2"
py="4"
px="2"
bgColor="gray.50"
spacing={6}
userSelect="none"
>
<Input placeholder="Search..." bgColor="white" />
<Stack>
<Text fontSize="sm" fontWeight="semibold" color="gray.600">
Bubbles
</Text>
<SimpleGrid columns={2} spacing="2">
{Object.values(BubbleStepType).map((type) => (
<StepCard key={type} type={type} onMouseDown={handleMouseDown} />
))}
</SimpleGrid>
</Stack>
<Stack>
<Text fontSize="sm" fontWeight="semibold" color="gray.600">
Inputs
</Text>
<SimpleGrid columns={2} spacing="2">
{Object.values(InputStepType).map((type) => (
<StepCard key={type} type={type} onMouseDown={handleMouseDown} />
))}
</SimpleGrid>
</Stack>
<Stack>
<Text fontSize="sm" fontWeight="semibold" color="gray.600">
Logic
</Text>
<SimpleGrid columns={2} spacing="2">
{Object.values(LogicStepType).map((type) => (
<StepCard key={type} type={type} onMouseDown={handleMouseDown} />
))}
</SimpleGrid>
</Stack>
<Stack>
<Text fontSize="sm" fontWeight="semibold" color="gray.600">
Integrations
</Text>
<SimpleGrid columns={2} spacing="2">
{Object.values(IntegrationStepType).map((type) => (
<StepCard key={type} type={type} onMouseDown={handleMouseDown} />
))}
</SimpleGrid>
</Stack>
{draggedStepType && (
<Portal>
<StepCardOverlay
type={draggedStepType}
onMouseUp={handleMouseUp}
pos="fixed"
top="0"
left="0"
style={{
transform: `translate(${position.x}px, ${position.y}px) rotate(-2deg)`,
}}
/>
</Portal>
)}
</Stack>
)
}

View File

@ -1 +0,0 @@
export { StepTypesList } from './StepTypesList'

View File

@ -31,31 +31,31 @@ type StepIconProps = { type: StepType } & IconProps
export const StepIcon = ({ type, ...props }: StepIconProps) => {
switch (type) {
case BubbleStepType.TEXT:
return <ChatIcon {...props} />
return <ChatIcon color="blue.500" {...props} />
case BubbleStepType.IMAGE:
return <ImageIcon {...props} />
return <ImageIcon color="blue.500" {...props} />
case BubbleStepType.VIDEO:
return <FilmIcon {...props} />
return <FilmIcon color="blue.500" {...props} />
case InputStepType.TEXT:
return <TextIcon {...props} />
return <TextIcon color="orange.500" {...props} />
case InputStepType.NUMBER:
return <NumberIcon {...props} />
return <NumberIcon color="orange.500" {...props} />
case InputStepType.EMAIL:
return <EmailIcon {...props} />
return <EmailIcon color="orange.500" {...props} />
case InputStepType.URL:
return <GlobeIcon {...props} />
return <GlobeIcon color="orange.500" {...props} />
case InputStepType.DATE:
return <CalendarIcon {...props} />
return <CalendarIcon color="orange.500" {...props} />
case InputStepType.PHONE:
return <PhoneIcon {...props} />
return <PhoneIcon color="orange.500" {...props} />
case InputStepType.CHOICE:
return <CheckSquareIcon {...props} />
return <CheckSquareIcon color="orange.500" {...props} />
case LogicStepType.SET_VARIABLE:
return <EditIcon {...props} />
return <EditIcon color="purple.500" {...props} />
case LogicStepType.CONDITION:
return <FilterIcon {...props} />
return <FilterIcon color="purple.500" {...props} />
case LogicStepType.REDIRECT:
return <ExternalLinkIcon {...props} />
return <ExternalLinkIcon color="purple.500" {...props} />
case IntegrationStepType.GOOGLE_SHEETS:
return <GoogleSheetsLogo {...props} />
case IntegrationStepType.GOOGLE_ANALYTICS:

View File

@ -0,0 +1,191 @@
import {
Stack,
Input,
Text,
SimpleGrid,
useEventListener,
Portal,
Flex,
IconButton,
Tooltip,
Fade,
} from '@chakra-ui/react'
import {
BubbleStepType,
DraggableStepType,
InputStepType,
IntegrationStepType,
LogicStepType,
} from 'models'
import { useStepDnd } from 'contexts/StepDndContext'
import React, { useState } from 'react'
import { StepCard, StepCardOverlay } from './StepCard'
import { LockedIcon, UnlockedIcon } from 'assets/icons'
import { headerHeight } from 'components/shared/TypebotHeader'
export const StepsSideBar = () => {
const { setDraggedStepType, draggedStepType } = useStepDnd()
const [position, setPosition] = useState({
x: 0,
y: 0,
})
const [relativeCoordinates, setRelativeCoordinates] = useState({ x: 0, y: 0 })
const [isLocked, setIsLocked] = useState(true)
const [isExtended, setIsExtended] = useState(true)
const handleMouseMove = (event: MouseEvent) => {
if (!draggedStepType) return
const { clientX, clientY } = event
setPosition({
...position,
x: clientX - relativeCoordinates.x,
y: clientY - relativeCoordinates.y,
})
}
useEventListener('mousemove', handleMouseMove)
const handleMouseDown = (e: React.MouseEvent, type: DraggableStepType) => {
const element = e.currentTarget as HTMLDivElement
const rect = element.getBoundingClientRect()
setPosition({ x: rect.left, y: rect.top })
const x = e.clientX - rect.left
const y = e.clientY - rect.top
setRelativeCoordinates({ x, y })
setDraggedStepType(type)
}
const handleMouseUp = () => {
if (!draggedStepType) return
setDraggedStepType(undefined)
setPosition({
x: 0,
y: 0,
})
}
useEventListener('mouseup', handleMouseUp)
const handleLockClick = () => setIsLocked(!isLocked)
const handleDockBarEnter = () => setIsExtended(true)
const handleMouseLeave = () => {
if (isLocked) return
setIsExtended(false)
}
return (
<Flex
w="340px"
pos="absolute"
left="0"
h={`calc(100vh - ${headerHeight}px)`}
zIndex="2"
pl="4"
py="4"
onMouseLeave={handleMouseLeave}
transform={isExtended ? 'translateX(0)' : 'translateX(-340px)'}
transition="transform 350ms cubic-bezier(0.075, 0.82, 0.165, 1) 0s"
>
<Stack
w="full"
rounded="lg"
shadow="xl"
borderWidth="1px"
pt="2"
pb="4"
px="2"
bgColor="gray.50"
spacing={6}
userSelect="none"
>
<Stack>
<Flex justifyContent="flex-end">
<Tooltip label={isLocked ? 'Unlock sidebar' : 'Lock sidebar'}>
<IconButton
icon={isLocked ? <LockedIcon /> : <UnlockedIcon />}
aria-label={isLocked ? 'Unlock' : 'Lock'}
size="sm"
variant="outline"
onClick={handleLockClick}
/>
</Tooltip>
</Flex>
<Input placeholder="Search..." bgColor="white" />
</Stack>
<Stack>
<Text fontSize="sm" fontWeight="semibold" color="gray.600">
Bubbles
</Text>
<SimpleGrid columns={2} spacing="2">
{Object.values(BubbleStepType).map((type) => (
<StepCard key={type} type={type} onMouseDown={handleMouseDown} />
))}
</SimpleGrid>
</Stack>
<Stack>
<Text fontSize="sm" fontWeight="semibold" color="gray.600">
Inputs
</Text>
<SimpleGrid columns={2} spacing="2">
{Object.values(InputStepType).map((type) => (
<StepCard key={type} type={type} onMouseDown={handleMouseDown} />
))}
</SimpleGrid>
</Stack>
<Stack>
<Text fontSize="sm" fontWeight="semibold" color="gray.600">
Logic
</Text>
<SimpleGrid columns={2} spacing="2">
{Object.values(LogicStepType).map((type) => (
<StepCard key={type} type={type} onMouseDown={handleMouseDown} />
))}
</SimpleGrid>
</Stack>
<Stack>
<Text fontSize="sm" fontWeight="semibold" color="gray.600">
Integrations
</Text>
<SimpleGrid columns={2} spacing="2">
{Object.values(IntegrationStepType).map((type) => (
<StepCard key={type} type={type} onMouseDown={handleMouseDown} />
))}
</SimpleGrid>
</Stack>
{draggedStepType && (
<Portal>
<StepCardOverlay
type={draggedStepType}
onMouseUp={handleMouseUp}
pos="fixed"
top="0"
left="0"
style={{
transform: `translate(${position.x}px, ${position.y}px) rotate(-2deg)`,
}}
/>
</Portal>
)}
</Stack>
<Fade in={!isLocked} unmountOnExit>
<Flex
pos="absolute"
h="100%"
right="-50px"
w="50px"
top="0"
justify="center"
align="center"
onMouseEnter={handleDockBarEnter}
>
<Flex w="5px" h="20px" bgColor="gray.400" rounded="md" />
</Flex>
</Fade>
</Flex>
)
}

View File

@ -0,0 +1 @@
export { StepsSideBar } from './StepSideBar'

View File

@ -94,6 +94,7 @@ export const ChoiceItemNode = ({
>
{(ref, isOpened) => (
<Flex
ref={ref}
align="center"
pos="relative"
onMouseEnter={handleMouseEnter}
@ -109,7 +110,6 @@ export const ChoiceItemNode = ({
borderColor={isOpened ? 'blue.400' : 'gray.300'}
>
<Editable
ref={ref}
defaultValue={item.content ?? 'Click to edit'}
flex="1"
startWithEditView={!isDefined(item.content)}

View File

@ -27,7 +27,7 @@ export const ContentPopover = ({ step }: Props) => {
return (
<Portal>
<PopoverContent onMouseDown={handleMouseDown}>
<PopoverContent onMouseDown={handleMouseDown} w="500px">
<PopoverArrow />
<PopoverBody ref={ref} shadow="lg">
<StepContent step={step} />

View File

@ -9,7 +9,7 @@ import {
import React, { useEffect, useState } from 'react'
import { BubbleStep, DraggableStep, Step, TextBubbleStep } from 'models'
import { Coordinates, useGraph } from 'contexts/GraphContext'
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
import { StepIcon } from 'components/board/StepsSideBar/StepIcon'
import { isBubbleStep, isTextBubbleStep } from 'utils'
import { TextEditor } from './TextEditor/TextEditor'
import { StepNodeContent } from './StepNodeContent/StepNodeContent'
@ -152,7 +152,7 @@ export const StepNode = ({
renderMenu={() => <StepNodeContextMenu stepId={step.id} />}
>
{(ref, isOpened) => (
<Popover placement="left" isLazy isOpen={openedStepId === step.id}>
<Popover placement="bottom" isLazy isOpen={openedStepId === step.id}>
<PopoverTrigger>
<Flex
pos="relative"

View File

@ -1,6 +1,6 @@
import { StackProps, HStack } from '@chakra-ui/react'
import { StartStep, Step } from 'models'
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
import { StepIcon } from 'components/board/StepsSideBar/StepIcon'
import { StepNodeContent } from './StepNodeContent/StepNodeContent'
export const StepNodeOverlay = ({

View File

@ -43,7 +43,7 @@ export const DropdownList = <T,>({
{currentItem ?? placeholder}
</MenuButton>
<Portal>
<MenuList maxW="500px" shadow="lg" zIndex={1500}>
<MenuList maxW="500px" zIndex={1500}>
<Stack maxH={'35vh'} overflowY="scroll" spacing="0">
{items.map((item) => (
<MenuItem

View File

@ -1,5 +1,5 @@
import { ChangeEvent, FormEvent, useEffect, useState } from 'react'
import { Button, HStack, Input, Stack } from '@chakra-ui/react'
import { ChangeEvent, useEffect, useState } from 'react'
import { Button, Flex, HStack, Input, Stack } from '@chakra-ui/react'
import { SearchContextManager } from '@giphy/react-components'
import { UploadButton } from '../buttons/UploadButton'
import { GiphySearch } from './GiphySearch'
@ -79,7 +79,7 @@ type ContentProps = { initialUrl?: string; onNewUrl: (url: string) => void }
const UploadFileContent = ({ onNewUrl }: ContentProps) => {
const { typebot } = useTypebot()
return (
<Stack>
<Flex justify="center" py="2">
<UploadButton
filePath={`typebots/${typebot?.id}`}
onFileUploaded={onNewUrl}
@ -88,7 +88,7 @@ const UploadFileContent = ({ onNewUrl }: ContentProps) => {
>
Choose an image
</UploadButton>
</Stack>
</Flex>
)
}
@ -106,11 +106,14 @@ const EmbedLinkContent = ({ initialUrl, onNewUrl }: ContentProps) => {
setImageUrl(e.target.value)
return (
<Stack py="2">
<Input
my="2"
placeholder={'Paste the image link...'}
onChange={handleImageUrlChange}
value={imageUrl}
/>
</Stack>
)
}

View File

@ -90,6 +90,14 @@ const components = {
_hover: { textDecoration: 'none' },
},
},
Menu: {
parts: ['list'],
defaultProps: {
list: {
shadow: 'lg',
},
},
},
}
export const customTheme = extendTheme({ colors, fonts, components })

View File

@ -12,7 +12,7 @@ const providers: Provider[] = [
EmailProvider({
server: {
host: process.env.EMAIL_SERVER_HOST,
port: process.env.EMAIL_SERVER_PORT,
port: Number(process.env.EMAIL_SERVER_PORT),
auth: {
user: process.env.EMAIL_SERVER_USER,
pass: process.env.EMAIL_SERVER_PASSWORD,

View File

@ -2,7 +2,7 @@ import { PublicTypebot, Typebot } from 'models'
import shortId from 'short-uuid'
import { HStack, Text } from '@chakra-ui/react'
import { CalendarIcon } from 'assets/icons'
import { StepIcon } from 'components/board/StepTypesList/StepIcon'
import { StepIcon } from 'components/board/StepsSideBar/StepIcon'
import { isInputStep, sendRequest } from 'utils'
export const parseTypebotToPublicTypebot = (