2
0

💄 Improve editor header responsiveness

Closes #1204
This commit is contained in:
Baptiste Arnaud
2024-03-07 10:09:00 +01:00
parent c2003dab91
commit 5dafb64963
2 changed files with 293 additions and 236 deletions

View File

@ -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>
)
}

View File

@ -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