♻️ (builder) Change to features-centric folder structure
This commit is contained in:
committed by
Baptiste Arnaud
parent
3686465a85
commit
643571fe7d
@@ -0,0 +1,83 @@
|
||||
import { Flex, HStack, StackProps, Text, Tooltip } from '@chakra-ui/react'
|
||||
import { BlockType, DraggableBlockType } from 'models'
|
||||
import { useBlockDnd } from '@/features/graph'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { BlockIcon } from './BlockIcon'
|
||||
import { BlockTypeLabel } from './BlockTypeLabel'
|
||||
|
||||
export const BlockCard = ({
|
||||
type,
|
||||
onMouseDown,
|
||||
isDisabled = false,
|
||||
}: {
|
||||
type: DraggableBlockType
|
||||
isDisabled?: boolean
|
||||
onMouseDown: (e: React.MouseEvent, type: DraggableBlockType) => void
|
||||
}) => {
|
||||
const { draggedBlockType } = useBlockDnd()
|
||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setIsMouseDown(draggedBlockType === type)
|
||||
}, [draggedBlockType, type])
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => onMouseDown(e, type)
|
||||
|
||||
return (
|
||||
<Tooltip label="Coming soon!" isDisabled={!isDisabled}>
|
||||
<Flex pos="relative">
|
||||
<HStack
|
||||
borderWidth="1px"
|
||||
borderColor="gray.200"
|
||||
rounded="lg"
|
||||
flex="1"
|
||||
cursor={'grab'}
|
||||
opacity={isMouseDown || isDisabled ? '0.4' : '1'}
|
||||
onMouseDown={handleMouseDown}
|
||||
bgColor="gray.50"
|
||||
px="4"
|
||||
py="2"
|
||||
_hover={{ shadow: 'md' }}
|
||||
transition="box-shadow 200ms"
|
||||
pointerEvents={isDisabled ? 'none' : 'auto'}
|
||||
>
|
||||
{!isMouseDown ? (
|
||||
<>
|
||||
<BlockIcon type={type} />
|
||||
<BlockTypeLabel type={type} />
|
||||
</>
|
||||
) : (
|
||||
<Text color="white" userSelect="none">
|
||||
Placeholder
|
||||
</Text>
|
||||
)}
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export const BlockCardOverlay = ({
|
||||
type,
|
||||
...props
|
||||
}: StackProps & { type: BlockType }) => {
|
||||
return (
|
||||
<HStack
|
||||
borderWidth="1px"
|
||||
rounded="lg"
|
||||
cursor={'grabbing'}
|
||||
w="147px"
|
||||
transition="none"
|
||||
pointerEvents="none"
|
||||
px="4"
|
||||
py="2"
|
||||
bgColor="white"
|
||||
shadow="xl"
|
||||
zIndex={2}
|
||||
{...props}
|
||||
>
|
||||
<BlockIcon type={type} />
|
||||
<BlockTypeLabel type={type} />
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { IconProps } from '@chakra-ui/react'
|
||||
import {
|
||||
BubbleBlockType,
|
||||
InputBlockType,
|
||||
IntegrationBlockType,
|
||||
LogicBlockType,
|
||||
BlockType,
|
||||
} from 'models'
|
||||
import React from 'react'
|
||||
import { TextBubbleIcon } from '@/features/blocks/bubbles/textBubble'
|
||||
import { ImageBubbleIcon } from '@/features/blocks/bubbles/image'
|
||||
import { VideoBubbleIcon } from '@/features/blocks/bubbles/video'
|
||||
import { ChatwootLogo } from '@/features/blocks/integrations/chatwoot'
|
||||
import { FlagIcon } from '@/components/icons'
|
||||
import { SendEmailIcon } from '@/features/blocks/integrations/sendEmail'
|
||||
import { PabblyConnectLogo } from '@/features/blocks/integrations/pabbly'
|
||||
import { MakeComLogo } from '@/features/blocks/integrations/makeCom'
|
||||
import { ZapierLogo } from '@/features/blocks/integrations/zapier'
|
||||
import { WebhookIcon } from '@/features/blocks/integrations/webhook'
|
||||
import { GoogleSheetsLogo } from '@/features/blocks/integrations/googleSheets'
|
||||
import { TypebotLinkIcon } from '@/features/blocks/logic/typebotLink'
|
||||
import { CodeIcon } from '@/features/blocks/logic/code'
|
||||
import { RedirectIcon } from '@/features/blocks/logic/redirect'
|
||||
import { ConditionIcon } from '@/features/blocks/logic/condition'
|
||||
import { SetVariableIcon } from '@/features/blocks/logic/setVariable'
|
||||
import { FileInputIcon } from '@/features/blocks/inputs/fileUpload'
|
||||
import { RatingInputIcon } from '@/features/blocks/inputs/rating'
|
||||
import { PaymentInputIcon } from '@/features/blocks/inputs/payment'
|
||||
import { ButtonsInputIcon } from '@/features/blocks/inputs/buttons'
|
||||
import { PhoneInputIcon } from '@/features/blocks/inputs/phone'
|
||||
import { DateInputIcon } from '@/features/blocks/inputs/date'
|
||||
import { UrlInputIcon } from '@/features/blocks/inputs/url'
|
||||
import { EmailInputIcon } from '@/features/blocks/inputs/emailInput'
|
||||
import { NumberInputIcon } from '@/features/blocks/inputs/number'
|
||||
import { TextInputIcon } from '@/features/blocks/inputs/textInput'
|
||||
import { EmbedBubbleIcon } from '@/features/blocks/bubbles/embed'
|
||||
import { GoogleAnalyticsLogo } from '@/features/blocks/integrations/googleAnalytics'
|
||||
|
||||
type BlockIconProps = { type: BlockType } & IconProps
|
||||
|
||||
export const BlockIcon = ({ type, ...props }: BlockIconProps) => {
|
||||
switch (type) {
|
||||
case BubbleBlockType.TEXT:
|
||||
return <TextBubbleIcon {...props} />
|
||||
case BubbleBlockType.IMAGE:
|
||||
return <ImageBubbleIcon {...props} />
|
||||
case BubbleBlockType.VIDEO:
|
||||
return <VideoBubbleIcon {...props} />
|
||||
case BubbleBlockType.EMBED:
|
||||
return <EmbedBubbleIcon color="blue.500" {...props} />
|
||||
case InputBlockType.TEXT:
|
||||
return <TextInputIcon color="orange.500" {...props} />
|
||||
case InputBlockType.NUMBER:
|
||||
return <NumberInputIcon color="orange.500" {...props} />
|
||||
case InputBlockType.EMAIL:
|
||||
return <EmailInputIcon color="orange.500" {...props} />
|
||||
case InputBlockType.URL:
|
||||
return <UrlInputIcon color="orange.500" {...props} />
|
||||
case InputBlockType.DATE:
|
||||
return <DateInputIcon color="orange.500" {...props} />
|
||||
case InputBlockType.PHONE:
|
||||
return <PhoneInputIcon color="orange.500" {...props} />
|
||||
case InputBlockType.CHOICE:
|
||||
return <ButtonsInputIcon color="orange.500" {...props} />
|
||||
case InputBlockType.PAYMENT:
|
||||
return <PaymentInputIcon color="orange.500" {...props} />
|
||||
case InputBlockType.RATING:
|
||||
return <RatingInputIcon color="orange.500" {...props} />
|
||||
case InputBlockType.FILE:
|
||||
return <FileInputIcon color="orange.500" {...props} />
|
||||
case LogicBlockType.SET_VARIABLE:
|
||||
return <SetVariableIcon color="purple.500" {...props} />
|
||||
case LogicBlockType.CONDITION:
|
||||
return <ConditionIcon color="purple.500" {...props} />
|
||||
case LogicBlockType.REDIRECT:
|
||||
return <RedirectIcon color="purple.500" {...props} />
|
||||
case LogicBlockType.CODE:
|
||||
return <CodeIcon color="purple.500" {...props} />
|
||||
case LogicBlockType.TYPEBOT_LINK:
|
||||
return <TypebotLinkIcon color="purple.500" {...props} />
|
||||
case IntegrationBlockType.GOOGLE_SHEETS:
|
||||
return <GoogleSheetsLogo {...props} />
|
||||
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
||||
return <GoogleAnalyticsLogo {...props} />
|
||||
case IntegrationBlockType.WEBHOOK:
|
||||
return <WebhookIcon {...props} />
|
||||
case IntegrationBlockType.ZAPIER:
|
||||
return <ZapierLogo {...props} />
|
||||
case IntegrationBlockType.MAKE_COM:
|
||||
return <MakeComLogo {...props} />
|
||||
case IntegrationBlockType.PABBLY_CONNECT:
|
||||
return <PabblyConnectLogo {...props} />
|
||||
case IntegrationBlockType.EMAIL:
|
||||
return <SendEmailIcon {...props} />
|
||||
case IntegrationBlockType.CHATWOOT:
|
||||
return <ChatwootLogo {...props} />
|
||||
case 'start':
|
||||
return <FlagIcon {...props} />
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { HStack, Text, Tooltip } from '@chakra-ui/react'
|
||||
import { useWorkspace } from '@/features/workspace'
|
||||
import { Plan } from 'db'
|
||||
import {
|
||||
BubbleBlockType,
|
||||
InputBlockType,
|
||||
IntegrationBlockType,
|
||||
LogicBlockType,
|
||||
BlockType,
|
||||
} from 'models'
|
||||
import React from 'react'
|
||||
import { isFreePlan, LockTag } from '@/features/billing'
|
||||
|
||||
type Props = { type: BlockType }
|
||||
|
||||
export const BlockTypeLabel = ({ type }: Props): JSX.Element => {
|
||||
const { workspace } = useWorkspace()
|
||||
|
||||
switch (type) {
|
||||
case 'start':
|
||||
return <Text>Start</Text>
|
||||
case BubbleBlockType.TEXT:
|
||||
case InputBlockType.TEXT:
|
||||
return <Text>Text</Text>
|
||||
case BubbleBlockType.IMAGE:
|
||||
return <Text>Image</Text>
|
||||
case BubbleBlockType.VIDEO:
|
||||
return <Text>Video</Text>
|
||||
case BubbleBlockType.EMBED:
|
||||
return (
|
||||
<Tooltip label="Embed a pdf, an iframe, a website...">
|
||||
<Text>Embed</Text>
|
||||
</Tooltip>
|
||||
)
|
||||
case InputBlockType.NUMBER:
|
||||
return <Text>Number</Text>
|
||||
case InputBlockType.EMAIL:
|
||||
return <Text>Email</Text>
|
||||
case InputBlockType.URL:
|
||||
return <Text>Website</Text>
|
||||
case InputBlockType.DATE:
|
||||
return <Text>Date</Text>
|
||||
case InputBlockType.PHONE:
|
||||
return <Text>Phone</Text>
|
||||
case InputBlockType.CHOICE:
|
||||
return <Text>Button</Text>
|
||||
case InputBlockType.PAYMENT:
|
||||
return <Text>Payment</Text>
|
||||
case InputBlockType.RATING:
|
||||
return <Text>Rating</Text>
|
||||
case InputBlockType.FILE:
|
||||
return (
|
||||
<Tooltip label="Upload Files">
|
||||
<HStack>
|
||||
<Text>File</Text>
|
||||
{isFreePlan(workspace) && <LockTag plan={Plan.STARTER} />}
|
||||
</HStack>
|
||||
</Tooltip>
|
||||
)
|
||||
case LogicBlockType.SET_VARIABLE:
|
||||
return <Text>Set variable</Text>
|
||||
case LogicBlockType.CONDITION:
|
||||
return <Text>Condition</Text>
|
||||
case LogicBlockType.REDIRECT:
|
||||
return <Text>Redirect</Text>
|
||||
case LogicBlockType.CODE:
|
||||
return (
|
||||
<Tooltip label="Run Javascript code">
|
||||
<Text>Code</Text>
|
||||
</Tooltip>
|
||||
)
|
||||
case LogicBlockType.TYPEBOT_LINK:
|
||||
return (
|
||||
<Tooltip label="Link to another of your typebots">
|
||||
<Text>Typebot</Text>
|
||||
</Tooltip>
|
||||
)
|
||||
case IntegrationBlockType.GOOGLE_SHEETS:
|
||||
return (
|
||||
<Tooltip label="Google Sheets">
|
||||
<Text>Sheets</Text>
|
||||
</Tooltip>
|
||||
)
|
||||
case IntegrationBlockType.GOOGLE_ANALYTICS:
|
||||
return (
|
||||
<Tooltip label="Google Analytics">
|
||||
<Text>Analytics</Text>
|
||||
</Tooltip>
|
||||
)
|
||||
case IntegrationBlockType.WEBHOOK:
|
||||
return <Text>Webhook</Text>
|
||||
case IntegrationBlockType.ZAPIER:
|
||||
return <Text>Zapier</Text>
|
||||
case IntegrationBlockType.MAKE_COM:
|
||||
return <Text>Make.com</Text>
|
||||
case IntegrationBlockType.PABBLY_CONNECT:
|
||||
return <Text>Pabbly</Text>
|
||||
case IntegrationBlockType.EMAIL:
|
||||
return <Text>Email</Text>
|
||||
case IntegrationBlockType.CHATWOOT:
|
||||
return <Text>Chatwoot</Text>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import {
|
||||
Stack,
|
||||
Text,
|
||||
SimpleGrid,
|
||||
useEventListener,
|
||||
Portal,
|
||||
Flex,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Fade,
|
||||
} from '@chakra-ui/react'
|
||||
import {
|
||||
BubbleBlockType,
|
||||
DraggableBlockType,
|
||||
InputBlockType,
|
||||
IntegrationBlockType,
|
||||
LogicBlockType,
|
||||
} from 'models'
|
||||
import { useBlockDnd } from '@/features/graph'
|
||||
import React, { useState } from 'react'
|
||||
import { BlockCard, BlockCardOverlay } from './BlockCard'
|
||||
import { LockedIcon, UnlockedIcon } from '@/components/icons'
|
||||
import { headerHeight } from '../../constants'
|
||||
|
||||
export const BlocksSideBar = () => {
|
||||
const { setDraggedBlockType, draggedBlockType } = useBlockDnd()
|
||||
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 (!draggedBlockType) return
|
||||
const { clientX, clientY } = event
|
||||
setPosition({
|
||||
...position,
|
||||
x: clientX - relativeCoordinates.x,
|
||||
y: clientY - relativeCoordinates.y,
|
||||
})
|
||||
}
|
||||
useEventListener('mousemove', handleMouseMove)
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent, type: DraggableBlockType) => {
|
||||
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 })
|
||||
setDraggedBlockType(type)
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (!draggedBlockType) return
|
||||
setDraggedBlockType(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="360px"
|
||||
pos="absolute"
|
||||
left="0"
|
||||
h={`calc(100vh - ${headerHeight}px)`}
|
||||
zIndex="2"
|
||||
pl="4"
|
||||
py="4"
|
||||
onMouseLeave={handleMouseLeave}
|
||||
transform={isExtended ? 'translateX(0)' : 'translateX(-350px)'}
|
||||
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="10"
|
||||
px="4"
|
||||
bgColor="white"
|
||||
spacing={6}
|
||||
userSelect="none"
|
||||
overflowY="scroll"
|
||||
className="hide-scrollbar"
|
||||
>
|
||||
<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>
|
||||
|
||||
<Stack>
|
||||
<Text fontSize="sm" fontWeight="semibold" color="gray.600">
|
||||
Bubbles
|
||||
</Text>
|
||||
<SimpleGrid columns={2} spacing="3">
|
||||
{Object.values(BubbleBlockType).map((type) => (
|
||||
<BlockCard key={type} type={type} onMouseDown={handleMouseDown} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
|
||||
<Stack>
|
||||
<Text fontSize="sm" fontWeight="semibold" color="gray.600">
|
||||
Inputs
|
||||
</Text>
|
||||
<SimpleGrid columns={2} spacing="3">
|
||||
{Object.values(InputBlockType).map((type) => (
|
||||
<BlockCard key={type} type={type} onMouseDown={handleMouseDown} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
|
||||
<Stack>
|
||||
<Text fontSize="sm" fontWeight="semibold" color="gray.600">
|
||||
Logic
|
||||
</Text>
|
||||
<SimpleGrid columns={2} spacing="3">
|
||||
{Object.values(LogicBlockType).map((type) => (
|
||||
<BlockCard key={type} type={type} onMouseDown={handleMouseDown} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
|
||||
<Stack>
|
||||
<Text fontSize="sm" fontWeight="semibold" color="gray.600">
|
||||
Integrations
|
||||
</Text>
|
||||
<SimpleGrid columns={2} spacing="3">
|
||||
{Object.values(IntegrationBlockType).map((type) => (
|
||||
<BlockCard
|
||||
key={type}
|
||||
type={type}
|
||||
onMouseDown={handleMouseDown}
|
||||
isDisabled={type === IntegrationBlockType.MAKE_COM}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
|
||||
{draggedBlockType && (
|
||||
<Portal>
|
||||
<BlockCardOverlay
|
||||
type={draggedBlockType}
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { BlocksSideBar } from './BlocksSideBar'
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuButtonProps,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react'
|
||||
import assert from 'assert'
|
||||
import {
|
||||
DownloadIcon,
|
||||
MoreVerticalIcon,
|
||||
SettingsIcon,
|
||||
} from '@/components/icons'
|
||||
import { useTypebot } from '../providers/TypebotProvider'
|
||||
import { useUser } from '@/features/account'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { isNotDefined } from 'utils'
|
||||
import { EditorSettingsModal } from './EditorSettingsModal'
|
||||
import { parseDefaultPublicId } from '@/features/publish'
|
||||
|
||||
export const BoardMenuButton = (props: MenuButtonProps) => {
|
||||
const { query } = useRouter()
|
||||
const { typebot } = useTypebot()
|
||||
const { user } = useUser()
|
||||
const [isDownloading, setIsDownloading] = useState(false)
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
user &&
|
||||
isNotDefined(user.graphNavigation) &&
|
||||
isNotDefined(query.isFirstBot)
|
||||
)
|
||||
onOpen()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const downloadFlow = () => {
|
||||
assert(typebot)
|
||||
setIsDownloading(true)
|
||||
const data =
|
||||
'data:application/json;charset=utf-8,' +
|
||||
encodeURIComponent(JSON.stringify(typebot))
|
||||
const fileName = `typebot-export-${parseDefaultPublicId(
|
||||
typebot.name,
|
||||
typebot.id
|
||||
)}.json`
|
||||
const linkElement = document.createElement('a')
|
||||
linkElement.setAttribute('href', data)
|
||||
linkElement.setAttribute('download', fileName)
|
||||
linkElement.click()
|
||||
setIsDownloading(false)
|
||||
}
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
bgColor="white"
|
||||
icon={<MoreVerticalIcon transform={'rotate(90deg)'} />}
|
||||
isLoading={isDownloading}
|
||||
size="sm"
|
||||
shadow="lg"
|
||||
{...props}
|
||||
/>
|
||||
<MenuList>
|
||||
<MenuItem icon={<SettingsIcon />} onClick={onOpen}>
|
||||
Editor settings
|
||||
</MenuItem>
|
||||
<MenuItem icon={<DownloadIcon />} onClick={downloadFlow}>
|
||||
Export flow
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
<EditorSettingsModal isOpen={isOpen} onClose={onClose} />
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Seo } from '@/components/Seo'
|
||||
import {
|
||||
Graph,
|
||||
GraphDndProvider,
|
||||
GraphProvider,
|
||||
GroupsCoordinatesProvider,
|
||||
} from '@/features/graph'
|
||||
import { Flex, Spinner } from '@chakra-ui/react'
|
||||
import {
|
||||
EditorProvider,
|
||||
useEditor,
|
||||
RightPanel as RightPanelEnum,
|
||||
} from '../providers/EditorProvider'
|
||||
import { useTypebot } from '../providers/TypebotProvider'
|
||||
import { BlocksSideBar } from './BlocksSideBar'
|
||||
import { BoardMenuButton } from './BoardMenuButton'
|
||||
import { GettingStartedModal } from './GettingStartedModal'
|
||||
import { PreviewDrawer } from './PreviewDrawer'
|
||||
import { TypebotHeader } from './TypebotHeader'
|
||||
|
||||
export const EditTypebotPage = () => {
|
||||
const { typebot, isReadOnly } = useTypebot()
|
||||
|
||||
return (
|
||||
<EditorProvider>
|
||||
<Seo title="Editor" />
|
||||
<Flex overflow="clip" h="100vh" flexDir="column" id="editor-container">
|
||||
<GettingStartedModal />
|
||||
<TypebotHeader />
|
||||
<Flex
|
||||
flex="1"
|
||||
pos="relative"
|
||||
h="full"
|
||||
background="#f4f5f8"
|
||||
backgroundImage="radial-gradient(#c6d0e1 1px, transparent 0)"
|
||||
backgroundSize="40px 40px"
|
||||
backgroundPosition="-19px -19px"
|
||||
>
|
||||
{typebot ? (
|
||||
<GraphDndProvider>
|
||||
<BlocksSideBar />
|
||||
<GraphProvider isReadOnly={isReadOnly}>
|
||||
<GroupsCoordinatesProvider groups={typebot.groups}>
|
||||
<Graph flex="1" typebot={typebot} />
|
||||
<BoardMenuButton pos="absolute" right="40px" top="20px" />
|
||||
<RightPanel />
|
||||
</GroupsCoordinatesProvider>
|
||||
</GraphProvider>
|
||||
</GraphDndProvider>
|
||||
) : (
|
||||
<Flex
|
||||
justify="center"
|
||||
align="center"
|
||||
boxSize="full"
|
||||
bgColor="rgba(255, 255, 255, 0.5)"
|
||||
>
|
||||
<Spinner color="gray" />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</EditorProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const RightPanel = () => {
|
||||
const { rightPanel } = useEditor()
|
||||
return rightPanel === RightPanelEnum.PREVIEW ? <PreviewDrawer /> : <></>
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
Stack,
|
||||
Heading,
|
||||
HStack,
|
||||
Text,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
VStack,
|
||||
} from '@chakra-ui/react'
|
||||
import { MouseIcon, LaptopIcon } from '@/components/icons'
|
||||
import { useUser } from '@/features/account'
|
||||
import { GraphNavigation } from 'db'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
export const EditorSettingsForm = () => {
|
||||
const { user, saveUser } = useUser()
|
||||
const [value, setValue] = useState<string>(
|
||||
user?.graphNavigation ?? GraphNavigation.TRACKPAD
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.graphNavigation === value) return
|
||||
saveUser({ graphNavigation: value as GraphNavigation }).then()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value])
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: GraphNavigation.MOUSE,
|
||||
label: 'Mouse',
|
||||
description:
|
||||
'Move by dragging the board and zoom in/out using the scroll wheel',
|
||||
icon: <MouseIcon boxSize="35px" />,
|
||||
},
|
||||
{
|
||||
value: GraphNavigation.TRACKPAD,
|
||||
label: 'Trackpad',
|
||||
description: 'Move the board using 2 fingers and zoom in/out by pinching',
|
||||
icon: <LaptopIcon boxSize="35px" />,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Stack spacing={6}>
|
||||
<Heading size="md">Editor Navigation</Heading>
|
||||
<RadioGroup onChange={setValue} value={value}>
|
||||
<HStack spacing={4} w="full" align="stretch">
|
||||
{options.map((option) => (
|
||||
<VStack
|
||||
key={option.value}
|
||||
as="label"
|
||||
htmlFor={option.label}
|
||||
cursor="pointer"
|
||||
borderWidth="1px"
|
||||
borderRadius="md"
|
||||
w="full"
|
||||
p="6"
|
||||
spacing={6}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<VStack spacing={6}>
|
||||
{option.icon}
|
||||
<Stack>
|
||||
<Text fontWeight="bold">{option.label}</Text>
|
||||
<Text>{option.description}</Text>
|
||||
</Stack>
|
||||
</VStack>
|
||||
|
||||
<Radio value={option.value} id={option.label} />
|
||||
</VStack>
|
||||
))}
|
||||
</HStack>
|
||||
</RadioGroup>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalOverlay,
|
||||
} from '@chakra-ui/react'
|
||||
import React from 'react'
|
||||
import { EditorSettingsForm } from './EditorSettingsForm'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export const EditorSettingsModal = ({ isOpen, onClose }: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalCloseButton />
|
||||
<ModalBody pt="12" pb="8" px="8">
|
||||
<EditorSettingsForm />
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import {
|
||||
useDisclosure,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalCloseButton,
|
||||
ModalBody,
|
||||
Stack,
|
||||
Heading,
|
||||
List,
|
||||
ListItem,
|
||||
Text,
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
HStack,
|
||||
Flex,
|
||||
} from '@chakra-ui/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export const GettingStartedModal = () => {
|
||||
const { query } = useRouter()
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
|
||||
useEffect(() => {
|
||||
if (query.isFirstBot) onOpen()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalCloseButton />
|
||||
<ModalBody as={Stack} spacing="8" py="10">
|
||||
<Stack spacing={4}>
|
||||
<Heading fontSize="xl">Editor basics</Heading>
|
||||
<List spacing={4}>
|
||||
<HStack as={ListItem}>
|
||||
<Flex
|
||||
bgColor="blue.500"
|
||||
rounded="full"
|
||||
boxSize="25px"
|
||||
justify="center"
|
||||
align="center"
|
||||
color="white"
|
||||
fontWeight="bold"
|
||||
flexShrink={0}
|
||||
fontSize="13px"
|
||||
>
|
||||
1
|
||||
</Flex>
|
||||
<Text>
|
||||
The left side bar contains blocks that you can drag and drop
|
||||
to the board.
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack as={ListItem}>
|
||||
<Flex
|
||||
bgColor="blue.500"
|
||||
rounded="full"
|
||||
boxSize="25px"
|
||||
fontSize="13px"
|
||||
justify="center"
|
||||
align="center"
|
||||
color="white"
|
||||
fontWeight="bold"
|
||||
flexShrink={0}
|
||||
>
|
||||
2
|
||||
</Flex>
|
||||
<Text>
|
||||
You can group blocks together by dropping them below or above
|
||||
each other
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack as={ListItem}>
|
||||
<Flex
|
||||
bgColor="blue.500"
|
||||
rounded="full"
|
||||
boxSize="25px"
|
||||
justify="center"
|
||||
align="center"
|
||||
color="white"
|
||||
fontWeight="bold"
|
||||
flexShrink={0}
|
||||
fontSize="13px"
|
||||
>
|
||||
3
|
||||
</Flex>
|
||||
<Text>Connect the groups together</Text>
|
||||
</HStack>
|
||||
<HStack as={ListItem}>
|
||||
<Flex
|
||||
bgColor="blue.500"
|
||||
rounded="full"
|
||||
boxSize="25px"
|
||||
justify="center"
|
||||
align="center"
|
||||
color="white"
|
||||
fontWeight="bold"
|
||||
flexShrink={0}
|
||||
fontSize="13px"
|
||||
>
|
||||
4
|
||||
</Flex>
|
||||
<Text>
|
||||
Preview your bot by clicking the preview button on the top
|
||||
right
|
||||
</Text>
|
||||
</HStack>
|
||||
</List>
|
||||
</Stack>
|
||||
|
||||
<Text>
|
||||
Feel free to use the bottom-right bubble to reach out if you have
|
||||
any question. I usually answer within the next 24 hours. 😃
|
||||
</Text>
|
||||
<Stack spacing={4}>
|
||||
<Heading fontSize="xl">See it in action ({`<`} 5 minutes)</Heading>
|
||||
<iframe
|
||||
width="100%"
|
||||
height="315"
|
||||
src="https://www.youtube.com/embed/jp3ggg_42-M"
|
||||
title="YouTube video player"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
style={{ borderRadius: '0.5rem' }}
|
||||
/>
|
||||
<Accordion allowToggle>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Box flex="1" textAlign="left">
|
||||
Other videos
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel py={10} as={Stack} spacing="10">
|
||||
<iframe
|
||||
width="100%"
|
||||
height="315"
|
||||
src="https://www.youtube.com/embed/6BudIC4GYNk"
|
||||
title="YouTube video player"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
style={{ borderRadius: '0.5rem' }}
|
||||
/>
|
||||
<iframe
|
||||
width="100%"
|
||||
height="315"
|
||||
src="https://www.youtube.com/embed/ZuyDwFLRbfQ"
|
||||
title="YouTube video player"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
style={{ borderRadius: '0.5rem' }}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</Stack>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
134
apps/builder/src/features/editor/components/PreviewDrawer.tsx
Normal file
134
apps/builder/src/features/editor/components/PreviewDrawer.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CloseButton,
|
||||
Fade,
|
||||
Flex,
|
||||
FlexProps,
|
||||
useEventListener,
|
||||
UseToastOptions,
|
||||
VStack,
|
||||
} from '@chakra-ui/react'
|
||||
import { TypebotViewer } from 'bot-engine'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { useEditor } from '../providers/EditorProvider'
|
||||
import { useGraph } from '@/features/graph'
|
||||
import { useTypebot } from '../providers/TypebotProvider'
|
||||
import { Log } from 'db'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { getViewerUrl } from 'utils'
|
||||
import { headerHeight } from '../constants'
|
||||
import { parseTypebotToPublicTypebot } from '@/features/publish'
|
||||
|
||||
export const PreviewDrawer = () => {
|
||||
const { typebot } = useTypebot()
|
||||
const { setRightPanel, startPreviewAtGroup } = useEditor()
|
||||
const { setPreviewingEdge } = useGraph()
|
||||
const [isResizing, setIsResizing] = useState(false)
|
||||
const [width, setWidth] = useState(500)
|
||||
const [isResizeHandleVisible, setIsResizeHandleVisible] = useState(false)
|
||||
const [restartKey, setRestartKey] = useState(0)
|
||||
|
||||
const publicTypebot = useMemo(
|
||||
() => (typebot ? { ...parseTypebotToPublicTypebot(typebot) } : undefined),
|
||||
[typebot]
|
||||
)
|
||||
|
||||
const { showToast } = useToast()
|
||||
|
||||
const handleMouseDown = () => {
|
||||
setIsResizing(true)
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!isResizing) return
|
||||
setWidth(width - e.movementX)
|
||||
}
|
||||
useEventListener('mousemove', handleMouseMove)
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsResizing(false)
|
||||
}
|
||||
useEventListener('mouseup', handleMouseUp)
|
||||
|
||||
const handleRestartClick = () => setRestartKey((key) => key + 1)
|
||||
|
||||
const handleCloseClick = () => {
|
||||
setPreviewingEdge(undefined)
|
||||
setRightPanel(undefined)
|
||||
}
|
||||
|
||||
const handleNewLog = (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) =>
|
||||
showToast(log as UseToastOptions)
|
||||
|
||||
return (
|
||||
<Flex
|
||||
pos="absolute"
|
||||
right="0"
|
||||
top={`0`}
|
||||
h={`100%`}
|
||||
w={`${width}px`}
|
||||
bgColor="white"
|
||||
shadow="lg"
|
||||
borderLeftRadius={'lg'}
|
||||
onMouseOver={() => setIsResizeHandleVisible(true)}
|
||||
onMouseLeave={() => setIsResizeHandleVisible(false)}
|
||||
p="6"
|
||||
zIndex={10}
|
||||
>
|
||||
<Fade in={isResizeHandleVisible}>
|
||||
<ResizeHandle
|
||||
pos="absolute"
|
||||
left="-7.5px"
|
||||
top={`calc(50% - ${headerHeight}px)`}
|
||||
onMouseDown={handleMouseDown}
|
||||
/>
|
||||
</Fade>
|
||||
|
||||
<VStack w="full" spacing={4}>
|
||||
<Flex justifyContent={'space-between'} w="full">
|
||||
<Button onClick={handleRestartClick}>Restart</Button>
|
||||
<CloseButton onClick={handleCloseClick} />
|
||||
</Flex>
|
||||
|
||||
{publicTypebot && (
|
||||
<Flex
|
||||
borderWidth={'1px'}
|
||||
borderRadius={'lg'}
|
||||
h="full"
|
||||
w="full"
|
||||
key={restartKey + (startPreviewAtGroup ?? '')}
|
||||
pointerEvents={isResizing ? 'none' : 'auto'}
|
||||
>
|
||||
<TypebotViewer
|
||||
apiHost={getViewerUrl({ isBuilder: true })}
|
||||
typebot={publicTypebot}
|
||||
onNewGroupVisible={setPreviewingEdge}
|
||||
onNewLog={handleNewLog}
|
||||
startGroupId={startPreviewAtGroup}
|
||||
isPreview
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
</VStack>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
const ResizeHandle = (props: FlexProps) => {
|
||||
return (
|
||||
<Flex
|
||||
w="15px"
|
||||
h="50px"
|
||||
borderWidth={'1px'}
|
||||
bgColor={'white'}
|
||||
cursor={'col-resize'}
|
||||
justifyContent={'center'}
|
||||
align={'center'}
|
||||
{...props}
|
||||
>
|
||||
<Box w="2px" bgColor={'gray.300'} h="70%" mr="0.5" />
|
||||
<Box w="2px" bgColor={'gray.300'} h="70%" />
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
Editable,
|
||||
EditablePreview,
|
||||
EditableInput,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
type EditableProps = {
|
||||
name: string
|
||||
onNewName: (newName: string) => void
|
||||
}
|
||||
export const EditableTypebotName = ({ name, onNewName }: EditableProps) => {
|
||||
const [localName, setLocalName] = useState(name)
|
||||
|
||||
useEffect(() => {
|
||||
if (name !== localName) setLocalName(name)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [name])
|
||||
|
||||
return (
|
||||
<Tooltip label="Rename">
|
||||
<Editable value={localName} onChange={setLocalName} onSubmit={onNewName}>
|
||||
<EditablePreview
|
||||
noOfLines={2}
|
||||
cursor="pointer"
|
||||
maxW="150px"
|
||||
overflow="hidden"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
fontSize="14px"
|
||||
/>
|
||||
<EditableInput fontSize="14px" />
|
||||
</Editable>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import {
|
||||
Flex,
|
||||
HStack,
|
||||
Button,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Spinner,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import {
|
||||
BuoyIcon,
|
||||
ChevronLeftIcon,
|
||||
RedoIcon,
|
||||
UndoIcon,
|
||||
} from '@/components/icons'
|
||||
import { RightPanel, useEditor } from '../../providers/EditorProvider'
|
||||
import { useTypebot } from '../../providers/TypebotProvider'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { isNotDefined } from 'utils'
|
||||
import { EditableTypebotName } from './EditableTypebotName'
|
||||
import { getBubbleActions } from 'typebot-js'
|
||||
import Link from 'next/link'
|
||||
import { isCloudProdInstance } from '@/utils/helpers'
|
||||
import { headerHeight } from '../../constants'
|
||||
import { EditableEmojiOrImageIcon } from '@/components/EditableEmojiOrImageIcon'
|
||||
import { PublishButton } from '@/features/publish'
|
||||
import { CollaborationMenuButton } from '@/features/collaboration'
|
||||
|
||||
export const TypebotHeader = () => {
|
||||
const router = useRouter()
|
||||
const {
|
||||
typebot,
|
||||
updateTypebot,
|
||||
save,
|
||||
undo,
|
||||
redo,
|
||||
canUndo,
|
||||
canRedo,
|
||||
isSavingLoading,
|
||||
} = useTypebot()
|
||||
const { setRightPanel, rightPanel, setStartPreviewAtGroup } = useEditor()
|
||||
|
||||
const handleNameSubmit = (name: string) => updateTypebot({ name })
|
||||
|
||||
const handleChangeIcon = (icon: string) => updateTypebot({ icon })
|
||||
|
||||
const handlePreviewClick = async () => {
|
||||
setStartPreviewAtGroup(undefined)
|
||||
save().then()
|
||||
setRightPanel(RightPanel.PREVIEW)
|
||||
}
|
||||
|
||||
const handleHelpClick = () => {
|
||||
isCloudProdInstance()
|
||||
? getBubbleActions().open()
|
||||
: window.open('https://docs.typebot.io', '_blank')
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
w="full"
|
||||
borderBottomWidth="1px"
|
||||
justify="center"
|
||||
align="center"
|
||||
h={`${headerHeight}px`}
|
||||
zIndex={100}
|
||||
pos="relative"
|
||||
bgColor="white"
|
||||
flexShrink={0}
|
||||
>
|
||||
<HStack
|
||||
display={['none', 'flex']}
|
||||
pos={{ base: 'absolute', xl: 'static' }}
|
||||
right={{ base: 280, xl: 0 }}
|
||||
>
|
||||
<Button
|
||||
as={Link}
|
||||
href={`/typebots/${typebot?.id}/edit`}
|
||||
colorScheme={router.pathname.includes('/edit') ? 'blue' : 'gray'}
|
||||
variant={router.pathname.includes('/edit') ? 'outline' : 'ghost'}
|
||||
size="sm"
|
||||
>
|
||||
Flow
|
||||
</Button>
|
||||
<Button
|
||||
as={Link}
|
||||
href={`/typebots/${typebot?.id}/theme`}
|
||||
colorScheme={router.pathname.endsWith('theme') ? 'blue' : 'gray'}
|
||||
variant={router.pathname.endsWith('theme') ? 'outline' : 'ghost'}
|
||||
size="sm"
|
||||
>
|
||||
Theme
|
||||
</Button>
|
||||
<Button
|
||||
as={Link}
|
||||
href={`/typebots/${typebot?.id}/settings`}
|
||||
colorScheme={router.pathname.endsWith('settings') ? 'blue' : 'gray'}
|
||||
variant={router.pathname.endsWith('settings') ? 'outline' : 'ghost'}
|
||||
size="sm"
|
||||
>
|
||||
Settings
|
||||
</Button>
|
||||
<Button
|
||||
as={Link}
|
||||
href={`/typebots/${typebot?.id}/share`}
|
||||
colorScheme={router.pathname.endsWith('share') ? 'blue' : 'gray'}
|
||||
variant={router.pathname.endsWith('share') ? 'outline' : 'ghost'}
|
||||
size="sm"
|
||||
>
|
||||
Share
|
||||
</Button>
|
||||
{typebot?.publishedTypebotId && (
|
||||
<Button
|
||||
as={Link}
|
||||
href={`/typebots/${typebot?.id}/results`}
|
||||
colorScheme={router.pathname.includes('results') ? 'blue' : 'gray'}
|
||||
variant={router.pathname.includes('results') ? 'outline' : 'ghost'}
|
||||
size="sm"
|
||||
>
|
||||
Results
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
<HStack
|
||||
pos="absolute"
|
||||
left="1rem"
|
||||
justify="center"
|
||||
align="center"
|
||||
spacing="6"
|
||||
>
|
||||
<HStack alignItems="center" spacing={3}>
|
||||
<IconButton
|
||||
as={Link}
|
||||
aria-label="Navigate back"
|
||||
icon={<ChevronLeftIcon fontSize={25} />}
|
||||
href={
|
||||
router.query.parentId
|
||||
? `/typebots/${router.query.parentId}/edit`
|
||||
: typebot?.folderId
|
||||
? `/typebots/folders/${typebot.folderId}`
|
||||
: '/typebots'
|
||||
}
|
||||
size="sm"
|
||||
/>
|
||||
<HStack spacing={1}>
|
||||
{typebot && (
|
||||
<EditableEmojiOrImageIcon
|
||||
uploadFilePath={`typebots/${typebot.id}/icon`}
|
||||
icon={typebot?.icon}
|
||||
onChangeIcon={handleChangeIcon}
|
||||
/>
|
||||
)}
|
||||
{typebot?.name && (
|
||||
<EditableTypebotName
|
||||
name={typebot?.name}
|
||||
onNewName={handleNameSubmit}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<HStack>
|
||||
<Tooltip label="Undo">
|
||||
<IconButton
|
||||
display={['none', 'flex']}
|
||||
icon={<UndoIcon />}
|
||||
size="sm"
|
||||
aria-label="Undo"
|
||||
onClick={undo}
|
||||
isDisabled={!canUndo}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Redo">
|
||||
<IconButton
|
||||
display={['none', 'flex']}
|
||||
icon={<RedoIcon />}
|
||||
size="sm"
|
||||
aria-label="Redo"
|
||||
onClick={redo}
|
||||
isDisabled={!canRedo}
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
<Button leftIcon={<BuoyIcon />} onClick={handleHelpClick} size="sm">
|
||||
Help
|
||||
</Button>
|
||||
</HStack>
|
||||
{isSavingLoading && (
|
||||
<HStack>
|
||||
<Spinner speed="0.7s" size="sm" color="gray.400" />
|
||||
<Text fontSize="sm" color="gray.400">
|
||||
Saving...
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<HStack right="40px" pos="absolute" display={['none', 'flex']}>
|
||||
<CollaborationMenuButton isLoading={isNotDefined(typebot)} />
|
||||
{router.pathname.includes('/edit') && isNotDefined(rightPanel) && (
|
||||
<Button
|
||||
onClick={handlePreviewClick}
|
||||
isLoading={isNotDefined(typebot)}
|
||||
size="sm"
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
)}
|
||||
<PublishButton size="sm" />
|
||||
</HStack>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './TypebotHeader'
|
||||
Reference in New Issue
Block a user