@ -8,6 +8,8 @@ import {
|
||||
Text,
|
||||
useColorModeValue,
|
||||
useDisclosure,
|
||||
StackProps,
|
||||
chakra,
|
||||
} from '@chakra-ui/react'
|
||||
import {
|
||||
BuoyIcon,
|
||||
@ -37,70 +39,12 @@ import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
import { Plan } from '@typebot.io/prisma'
|
||||
|
||||
export const TypebotHeader = () => {
|
||||
const { t } = useTranslate()
|
||||
const router = useRouter()
|
||||
const {
|
||||
typebot,
|
||||
publishedTypebot,
|
||||
updateTypebot,
|
||||
save,
|
||||
undo,
|
||||
redo,
|
||||
canUndo,
|
||||
canRedo,
|
||||
isSavingLoading,
|
||||
currentUserMode,
|
||||
} = useTypebot()
|
||||
const { typebot, publishedTypebot, currentUserMode } = useTypebot()
|
||||
const { workspace } = useWorkspace()
|
||||
const {
|
||||
setRightPanel,
|
||||
rightPanel,
|
||||
setStartPreviewAtGroup,
|
||||
setStartPreviewAtEvent,
|
||||
} = useEditor()
|
||||
const [isUndoShortcutTooltipOpen, setUndoShortcutTooltipOpen] =
|
||||
useState(false)
|
||||
const hideUndoShortcutTooltipLater = useDebouncedCallback(() => {
|
||||
setUndoShortcutTooltipOpen(false)
|
||||
}, 1000)
|
||||
const [isRedoShortcutTooltipOpen, setRedoShortcutTooltipOpen] =
|
||||
useState(false)
|
||||
const hideRedoShortcutTooltipLater = useDebouncedCallback(() => {
|
||||
setRedoShortcutTooltipOpen(false)
|
||||
}, 1000)
|
||||
|
||||
const { isOpen, onOpen } = useDisclosure()
|
||||
const headerBgColor = useColorModeValue('white', 'gray.900')
|
||||
|
||||
const handleNameSubmit = (name: string) =>
|
||||
updateTypebot({ updates: { name } })
|
||||
|
||||
const handleChangeIcon = (icon: string) =>
|
||||
updateTypebot({ updates: { icon } })
|
||||
|
||||
const handlePreviewClick = async () => {
|
||||
setStartPreviewAtGroup(undefined)
|
||||
setStartPreviewAtEvent(undefined)
|
||||
save().then()
|
||||
setRightPanel(RightPanel.PREVIEW)
|
||||
}
|
||||
|
||||
useKeyboardShortcuts({
|
||||
undo: () => {
|
||||
if (!canUndo) return
|
||||
hideUndoShortcutTooltipLater.flush()
|
||||
setUndoShortcutTooltipOpen(true)
|
||||
hideUndoShortcutTooltipLater()
|
||||
undo()
|
||||
},
|
||||
redo: () => {
|
||||
if (!canRedo) return
|
||||
hideUndoShortcutTooltipLater.flush()
|
||||
setRedoShortcutTooltipOpen(true)
|
||||
hideRedoShortcutTooltipLater()
|
||||
redo()
|
||||
},
|
||||
})
|
||||
|
||||
const handleHelpClick = () => {
|
||||
isCloudProdInstance() && workspace?.plan && workspace.plan !== Plan.FREE
|
||||
? onOpen()
|
||||
@ -121,182 +65,291 @@ export const TypebotHeader = () => {
|
||||
flexShrink={0}
|
||||
>
|
||||
{isOpen && <SupportBubble autoShowDelay={0} />}
|
||||
<HStack
|
||||
display={['none', 'flex']}
|
||||
pos={{ base: 'absolute', xl: 'static' }}
|
||||
right={{ base: isDefined(publishedTypebot) ? 340 : 295, 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"
|
||||
>
|
||||
{t('editor.header.flowButton.label')}
|
||||
</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"
|
||||
>
|
||||
{t('editor.header.themeButton.label')}
|
||||
</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"
|
||||
>
|
||||
{t('editor.header.settingsButton.label')}
|
||||
</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"
|
||||
>
|
||||
{t('share.button.label')}
|
||||
</Button>
|
||||
{isDefined(publishedTypebot) && (
|
||||
<Button
|
||||
as={Link}
|
||||
href={`/typebots/${typebot?.id}/results`}
|
||||
colorScheme={router.pathname.includes('results') ? 'blue' : 'gray'}
|
||||
variant={router.pathname.includes('results') ? 'outline' : 'ghost'}
|
||||
size="sm"
|
||||
>
|
||||
{t('editor.header.resultsButton.label')}
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
<HStack
|
||||
<LeftElements pos="absolute" left="1rem" onHelpClick={handleHelpClick} />
|
||||
<TypebotNav
|
||||
display={{ base: 'none', xl: 'flex' }}
|
||||
pos={{ base: 'absolute' }}
|
||||
typebotId={typebot?.id}
|
||||
isResultsDisplayed={isDefined(publishedTypebot)}
|
||||
/>
|
||||
<RightElements
|
||||
right="40px"
|
||||
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={{
|
||||
pathname: router.query.parentId
|
||||
? '/typebots/[typebotId]/edit'
|
||||
: typebot?.folderId
|
||||
? '/typebots/folders/[folderId]'
|
||||
: '/typebots',
|
||||
query: {
|
||||
folderId: typebot?.folderId ?? [],
|
||||
parentId: Array.isArray(router.query.parentId)
|
||||
? router.query.parentId.slice(0, -1)
|
||||
: [],
|
||||
typebotId: Array.isArray(router.query.parentId)
|
||||
? [...router.query.parentId].pop()
|
||||
: router.query.parentId ?? [],
|
||||
},
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
<HStack spacing={1}>
|
||||
{typebot && (
|
||||
<EditableEmojiOrImageIcon
|
||||
uploadFileProps={{
|
||||
workspaceId: typebot.workspaceId,
|
||||
typebotId: typebot.id,
|
||||
fileName: 'icon',
|
||||
}}
|
||||
icon={typebot?.icon}
|
||||
onChangeIcon={handleChangeIcon}
|
||||
/>
|
||||
)}
|
||||
(
|
||||
<EditableTypebotName
|
||||
key={`typebot-name-${typebot?.name ?? ''}`}
|
||||
defaultName={typebot?.name ?? ''}
|
||||
onNewName={handleNameSubmit}
|
||||
/>
|
||||
)
|
||||
</HStack>
|
||||
|
||||
{currentUserMode === 'write' && (
|
||||
<HStack>
|
||||
<Tooltip
|
||||
label={
|
||||
isUndoShortcutTooltipOpen
|
||||
? t('editor.header.undo.tooltip.label')
|
||||
: t('editor.header.undoButton.label')
|
||||
}
|
||||
isOpen={isUndoShortcutTooltipOpen ? true : undefined}
|
||||
hasArrow={isUndoShortcutTooltipOpen}
|
||||
>
|
||||
<IconButton
|
||||
display={['none', 'flex']}
|
||||
icon={<UndoIcon />}
|
||||
size="sm"
|
||||
aria-label={t('editor.header.undoButton.label')}
|
||||
onClick={undo}
|
||||
isDisabled={!canUndo}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
label={
|
||||
isRedoShortcutTooltipOpen
|
||||
? t('editor.header.undo.tooltip.label')
|
||||
: t('editor.header.redoButton.label')
|
||||
}
|
||||
isOpen={isRedoShortcutTooltipOpen ? true : undefined}
|
||||
hasArrow={isRedoShortcutTooltipOpen}
|
||||
>
|
||||
<IconButton
|
||||
display={['none', 'flex']}
|
||||
icon={<RedoIcon />}
|
||||
size="sm"
|
||||
aria-label={t('editor.header.redoButton.label')}
|
||||
onClick={redo}
|
||||
isDisabled={!canRedo}
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
)}
|
||||
<Button leftIcon={<BuoyIcon />} onClick={handleHelpClick} size="sm">
|
||||
{t('editor.header.helpButton.label')}
|
||||
</Button>
|
||||
</HStack>
|
||||
{isSavingLoading && (
|
||||
<HStack>
|
||||
<Spinner speed="0.7s" size="sm" color="gray.400" />
|
||||
<Text fontSize="sm" color="gray.400">
|
||||
{t('editor.header.savingSpinner.label')}
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<HStack right="40px" pos="absolute" display={['none', 'flex']}>
|
||||
<Flex pos="relative">
|
||||
<ShareTypebotButton isLoading={isNotDefined(typebot)} />
|
||||
</Flex>
|
||||
{router.pathname.includes('/edit') && isNotDefined(rightPanel) && (
|
||||
<Button
|
||||
colorScheme="gray"
|
||||
onClick={handlePreviewClick}
|
||||
isLoading={isNotDefined(typebot)}
|
||||
leftIcon={<PlayIcon />}
|
||||
size="sm"
|
||||
>
|
||||
{t('editor.header.previewButton.label')}
|
||||
</Button>
|
||||
)}
|
||||
{currentUserMode === 'write' && <PublishButton size="sm" />}
|
||||
</HStack>
|
||||
display={['none', 'flex']}
|
||||
isResultsDisplayed={isDefined(publishedTypebot)}
|
||||
/>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
const LeftElements = (props: StackProps & { onHelpClick: () => void }) => {
|
||||
const { t } = useTranslate()
|
||||
const router = useRouter()
|
||||
const {
|
||||
typebot,
|
||||
updateTypebot,
|
||||
canUndo,
|
||||
canRedo,
|
||||
undo,
|
||||
redo,
|
||||
currentUserMode,
|
||||
isSavingLoading,
|
||||
} = useTypebot()
|
||||
|
||||
const [isRedoShortcutTooltipOpen, setRedoShortcutTooltipOpen] =
|
||||
useState(false)
|
||||
|
||||
const [isUndoShortcutTooltipOpen, setUndoShortcutTooltipOpen] =
|
||||
useState(false)
|
||||
|
||||
const hideUndoShortcutTooltipLater = useDebouncedCallback(() => {
|
||||
setUndoShortcutTooltipOpen(false)
|
||||
}, 1000)
|
||||
|
||||
const hideRedoShortcutTooltipLater = useDebouncedCallback(() => {
|
||||
setRedoShortcutTooltipOpen(false)
|
||||
}, 1000)
|
||||
|
||||
const handleNameSubmit = (name: string) =>
|
||||
updateTypebot({ updates: { name } })
|
||||
|
||||
const handleChangeIcon = (icon: string) =>
|
||||
updateTypebot({ updates: { icon } })
|
||||
|
||||
useKeyboardShortcuts({
|
||||
undo: () => {
|
||||
if (!canUndo) return
|
||||
hideUndoShortcutTooltipLater.flush()
|
||||
setUndoShortcutTooltipOpen(true)
|
||||
hideUndoShortcutTooltipLater()
|
||||
undo()
|
||||
},
|
||||
redo: () => {
|
||||
if (!canRedo) return
|
||||
hideUndoShortcutTooltipLater.flush()
|
||||
setRedoShortcutTooltipOpen(true)
|
||||
hideRedoShortcutTooltipLater()
|
||||
redo()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<HStack justify="center" align="center" spacing="6" {...props}>
|
||||
<HStack alignItems="center" spacing={3}>
|
||||
<IconButton
|
||||
as={Link}
|
||||
aria-label="Navigate back"
|
||||
icon={<ChevronLeftIcon fontSize={25} />}
|
||||
href={{
|
||||
pathname: router.query.parentId
|
||||
? '/typebots/[typebotId]/edit'
|
||||
: typebot?.folderId
|
||||
? '/typebots/folders/[folderId]'
|
||||
: '/typebots',
|
||||
query: {
|
||||
folderId: typebot?.folderId ?? [],
|
||||
parentId: Array.isArray(router.query.parentId)
|
||||
? router.query.parentId.slice(0, -1)
|
||||
: [],
|
||||
typebotId: Array.isArray(router.query.parentId)
|
||||
? [...router.query.parentId].pop()
|
||||
: router.query.parentId ?? [],
|
||||
},
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
<HStack spacing={1}>
|
||||
{typebot && (
|
||||
<EditableEmojiOrImageIcon
|
||||
uploadFileProps={{
|
||||
workspaceId: typebot.workspaceId,
|
||||
typebotId: typebot.id,
|
||||
fileName: 'icon',
|
||||
}}
|
||||
icon={typebot?.icon}
|
||||
onChangeIcon={handleChangeIcon}
|
||||
/>
|
||||
)}
|
||||
(
|
||||
<EditableTypebotName
|
||||
key={`typebot-name-${typebot?.name ?? ''}`}
|
||||
defaultName={typebot?.name ?? ''}
|
||||
onNewName={handleNameSubmit}
|
||||
/>
|
||||
)
|
||||
</HStack>
|
||||
|
||||
{currentUserMode === 'write' && (
|
||||
<HStack>
|
||||
<Tooltip
|
||||
label={
|
||||
isUndoShortcutTooltipOpen
|
||||
? t('editor.header.undo.tooltip.label')
|
||||
: t('editor.header.undoButton.label')
|
||||
}
|
||||
isOpen={isUndoShortcutTooltipOpen ? true : undefined}
|
||||
hasArrow={isUndoShortcutTooltipOpen}
|
||||
>
|
||||
<IconButton
|
||||
display={['none', 'flex']}
|
||||
icon={<UndoIcon />}
|
||||
size="sm"
|
||||
aria-label={t('editor.header.undoButton.label')}
|
||||
onClick={undo}
|
||||
isDisabled={!canUndo}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
label={
|
||||
isRedoShortcutTooltipOpen
|
||||
? t('editor.header.undo.tooltip.label')
|
||||
: t('editor.header.redoButton.label')
|
||||
}
|
||||
isOpen={isRedoShortcutTooltipOpen ? true : undefined}
|
||||
hasArrow={isRedoShortcutTooltipOpen}
|
||||
>
|
||||
<IconButton
|
||||
display={['none', 'flex']}
|
||||
icon={<RedoIcon />}
|
||||
size="sm"
|
||||
aria-label={t('editor.header.redoButton.label')}
|
||||
onClick={redo}
|
||||
isDisabled={!canRedo}
|
||||
/>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
)}
|
||||
<Button
|
||||
leftIcon={<BuoyIcon />}
|
||||
onClick={props.onHelpClick}
|
||||
size="sm"
|
||||
iconSpacing={{ base: 0, xl: 2 }}
|
||||
>
|
||||
<chakra.span display={{ base: 'none', xl: 'inline' }}>
|
||||
{t('editor.header.helpButton.label')}
|
||||
</chakra.span>
|
||||
</Button>
|
||||
</HStack>
|
||||
{isSavingLoading && (
|
||||
<HStack>
|
||||
<Spinner speed="0.7s" size="sm" color="gray.400" />
|
||||
<Text fontSize="sm" color="gray.400">
|
||||
{t('editor.header.savingSpinner.label')}
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
const RightElements = (props: StackProps & { isResultsDisplayed: boolean }) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslate()
|
||||
const { typebot, currentUserMode, save } = useTypebot()
|
||||
const {
|
||||
setRightPanel,
|
||||
rightPanel,
|
||||
setStartPreviewAtGroup,
|
||||
setStartPreviewAtEvent,
|
||||
} = useEditor()
|
||||
|
||||
const handlePreviewClick = async () => {
|
||||
setStartPreviewAtGroup(undefined)
|
||||
setStartPreviewAtEvent(undefined)
|
||||
save().then()
|
||||
setRightPanel(RightPanel.PREVIEW)
|
||||
}
|
||||
|
||||
return (
|
||||
<HStack {...props}>
|
||||
<TypebotNav
|
||||
display={{ base: 'none', md: 'flex', xl: 'none' }}
|
||||
typebotId={typebot?.id}
|
||||
isResultsDisplayed={props.isResultsDisplayed}
|
||||
/>
|
||||
<Flex pos="relative">
|
||||
<ShareTypebotButton isLoading={isNotDefined(typebot)} />
|
||||
</Flex>
|
||||
{router.pathname.includes('/edit') && isNotDefined(rightPanel) && (
|
||||
<Button
|
||||
colorScheme="gray"
|
||||
onClick={handlePreviewClick}
|
||||
isLoading={isNotDefined(typebot)}
|
||||
leftIcon={<PlayIcon />}
|
||||
size="sm"
|
||||
iconSpacing={{ base: 0, xl: 2 }}
|
||||
>
|
||||
<chakra.span display={{ base: 'none', xl: 'inline' }}>
|
||||
{t('editor.header.previewButton.label')}
|
||||
</chakra.span>
|
||||
</Button>
|
||||
)}
|
||||
{currentUserMode === 'write' && <PublishButton size="sm" />}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
const TypebotNav = ({
|
||||
typebotId,
|
||||
isResultsDisplayed,
|
||||
...stackProps
|
||||
}: {
|
||||
typebotId?: string
|
||||
isResultsDisplayed: boolean
|
||||
} & StackProps) => {
|
||||
const { t } = useTranslate()
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<HStack {...stackProps}>
|
||||
<Button
|
||||
as={Link}
|
||||
href={`/typebots/${typebotId}/edit`}
|
||||
colorScheme={router.pathname.includes('/edit') ? 'blue' : 'gray'}
|
||||
variant={router.pathname.includes('/edit') ? 'outline' : 'ghost'}
|
||||
size="sm"
|
||||
>
|
||||
{t('editor.header.flowButton.label')}
|
||||
</Button>
|
||||
<Button
|
||||
as={Link}
|
||||
href={`/typebots/${typebotId}/theme`}
|
||||
colorScheme={router.pathname.endsWith('theme') ? 'blue' : 'gray'}
|
||||
variant={router.pathname.endsWith('theme') ? 'outline' : 'ghost'}
|
||||
size="sm"
|
||||
>
|
||||
{t('editor.header.themeButton.label')}
|
||||
</Button>
|
||||
<Button
|
||||
as={Link}
|
||||
href={`/typebots/${typebotId}/settings`}
|
||||
colorScheme={router.pathname.endsWith('settings') ? 'blue' : 'gray'}
|
||||
variant={router.pathname.endsWith('settings') ? 'outline' : 'ghost'}
|
||||
size="sm"
|
||||
>
|
||||
{t('editor.header.settingsButton.label')}
|
||||
</Button>
|
||||
<Button
|
||||
as={Link}
|
||||
href={`/typebots/${typebotId}/share`}
|
||||
colorScheme={router.pathname.endsWith('share') ? 'blue' : 'gray'}
|
||||
variant={router.pathname.endsWith('share') ? 'outline' : 'ghost'}
|
||||
size="sm"
|
||||
>
|
||||
{t('share.button.label')}
|
||||
</Button>
|
||||
{isResultsDisplayed && (
|
||||
<Button
|
||||
as={Link}
|
||||
href={`/typebots/${typebotId}/results`}
|
||||
colorScheme={router.pathname.includes('results') ? 'blue' : 'gray'}
|
||||
variant={router.pathname.includes('results') ? 'outline' : 'ghost'}
|
||||
size="sm"
|
||||
>
|
||||
{t('editor.header.resultsButton.label')}
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
Button,
|
||||
chakra,
|
||||
} from '@chakra-ui/react'
|
||||
import { UsersIcon } from '@/components/icons'
|
||||
import React from 'react'
|
||||
@ -20,8 +21,11 @@ export const ShareTypebotButton = ({ isLoading }: { isLoading: boolean }) => {
|
||||
leftIcon={<UsersIcon />}
|
||||
aria-label={t('share.button.popover.ariaLabel')}
|
||||
size="sm"
|
||||
iconSpacing={{ base: 0, xl: 2 }}
|
||||
>
|
||||
{t('share.button.label')}
|
||||
<chakra.span display={{ base: 'none', xl: 'inline' }}>
|
||||
{t('share.button.label')}
|
||||
</chakra.span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
|
Reference in New Issue
Block a user