2
0

🚸 (editor) Show toolbar on group click

This commit is contained in:
Baptiste Arnaud
2023-02-23 16:11:51 +01:00
parent 2ff6991ca7
commit 0619c60970
4 changed files with 126 additions and 33 deletions

View File

@ -197,11 +197,19 @@ test('Preview from group should work', async ({ page }) => {
) )
await page.goto(`/typebots/${typebotId}/edit`) await page.goto(`/typebots/${typebotId}/edit`)
await page.click('[aria-label="Preview bot from this group"] >> nth=1') await page
.getByTestId('group')
.nth(1)
.click({ position: { x: 100, y: 10 } })
await page.click('[aria-label="Preview bot from this group"]')
await expect( await expect(
page.locator('typebot-standard').locator('text="Hello this is group 1"') page.locator('typebot-standard').locator('text="Hello this is group 1"')
).toBeVisible() ).toBeVisible()
await page.click('[aria-label="Preview bot from this group"] >> nth=2') await page
.getByTestId('group')
.nth(2)
.click({ position: { x: 100, y: 10 } })
await page.click('[aria-label="Preview bot from this group"]')
await expect( await expect(
page.locator('typebot-standard').locator('text="Hello this is group 2"') page.locator('typebot-standard').locator('text="Hello this is group 2"')
).toBeVisible() ).toBeVisible()

View File

@ -0,0 +1,55 @@
import { CopyIcon, PlayIcon, TrashIcon } from '@/components/icons'
import { HStack, IconButton, useColorModeValue } from '@chakra-ui/react'
type Props = {
onPlayClick: () => void
onDuplicateClick: () => void
onDeleteClick: () => void
}
export const GroupFocusToolbar = ({
onPlayClick,
onDuplicateClick,
onDeleteClick,
}: Props) => {
return (
<HStack
rounded="md"
spacing={0}
borderWidth="1px"
bgColor={useColorModeValue('white', 'gray.800')}
shadow="md"
>
<IconButton
icon={<PlayIcon />}
borderRightWidth="1px"
borderRightRadius="none"
aria-label={'Preview bot from this group'}
variant="ghost"
onClick={onPlayClick}
size="sm"
/>
<IconButton
icon={<CopyIcon />}
borderRightWidth="1px"
borderRightRadius="none"
borderLeftRadius="none"
aria-label={'Duplicate group'}
variant="ghost"
onClick={(e) => {
e.stopPropagation()
onDuplicateClick()
}}
size="sm"
/>
<IconButton
aria-label="Delete"
borderLeftRadius="none"
icon={<TrashIcon />}
onClick={onDeleteClick}
variant="ghost"
size="sm"
/>
</HStack>
)
}

View File

@ -2,7 +2,7 @@ import {
Editable, Editable,
EditableInput, EditableInput,
EditablePreview, EditablePreview,
IconButton, SlideFade,
Stack, Stack,
useColorModeValue, useColorModeValue,
} from '@chakra-ui/react' } from '@chakra-ui/react'
@ -18,11 +18,12 @@ import { BlockNodesList } from '../BlockNode/BlockNodesList'
import { isDefined, isNotDefined } from 'utils' import { isDefined, isNotDefined } from 'utils'
import { useTypebot, RightPanel, useEditor } from '@/features/editor' import { useTypebot, RightPanel, useEditor } from '@/features/editor'
import { GroupNodeContextMenu } from './GroupNodeContextMenu' import { GroupNodeContextMenu } from './GroupNodeContextMenu'
import { PlayIcon } from '@/components/icons'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
import { ContextMenu } from '@/components/ContextMenu' import { ContextMenu } from '@/components/ContextMenu'
import { setMultipleRefs } from '@/utils/helpers' import { setMultipleRefs } from '@/utils/helpers'
import { useDrag } from '@use-gesture/react' import { useDrag } from '@use-gesture/react'
import { GroupFocusToolbar } from './GroupFocusToolbar'
import { useOutsideClick } from '@/hooks/useOutsideClick'
type Props = { type Props = {
group: Group group: Group
@ -63,11 +64,9 @@ const NonMemoizedDraggableGroupNode = ({
previewingEdge, previewingEdge,
previewingBlock, previewingBlock,
isReadOnly, isReadOnly,
focusedGroupId,
setFocusedGroupId,
graphPosition, graphPosition,
} = useGraph() } = useGraph()
const { typebot, updateGroup } = useTypebot() const { typebot, updateGroup, deleteGroup, duplicateGroup } = useTypebot()
const { setMouseOverGroup, mouseOverGroup } = useBlockDnd() const { setMouseOverGroup, mouseOverGroup } = useBlockDnd()
const { setRightPanel, setStartPreviewAtGroup } = useEditor() const { setRightPanel, setStartPreviewAtGroup } = useEditor()
@ -78,6 +77,27 @@ const NonMemoizedDraggableGroupNode = ({
) )
const [groupTitle, setGroupTitle] = useState(group.title) const [groupTitle, setGroupTitle] = useState(group.title)
const isPreviewing =
previewingBlock?.groupId === group.id ||
previewingEdge?.from.groupId === group.id ||
(previewingEdge?.to.groupId === group.id &&
isNotDefined(previewingEdge.to.blockId))
const isStartGroup =
isDefined(group.blocks[0]) && group.blocks[0].type === 'start'
const groupRef = useRef<HTMLDivElement | null>(null)
const [debouncedGroupPosition] = useDebounce(currentCoordinates, 100)
const [isFocused, setIsFocused] = useState(false)
const [ignoreNextFocusIntent, setIgnoreNextFocusIntent] = useState(false)
useOutsideClick({
handler: () => setIsFocused(false),
ref: groupRef,
capture: true,
})
// When the group is moved from external action (e.g. undo/redo), update the current coordinates // When the group is moved from external action (e.g. undo/redo), update the current coordinates
useEffect(() => { useEffect(() => {
setCurrentCoordinates({ setCurrentCoordinates({
@ -90,17 +110,6 @@ const NonMemoizedDraggableGroupNode = ({
useEffect(() => { useEffect(() => {
setGroupTitle(group.title) setGroupTitle(group.title)
}, [group.title]) }, [group.title])
const isPreviewing =
previewingBlock?.groupId === group.id ||
previewingEdge?.from.groupId === group.id ||
(previewingEdge?.to.groupId === group.id &&
isNotDefined(previewingEdge.to.blockId))
const isStartGroup =
isDefined(group.blocks[0]) && group.blocks[0].type === 'start'
const groupRef = useRef<HTMLDivElement | null>(null)
const [debouncedGroupPosition] = useDebounce(currentCoordinates, 100)
useEffect(() => { useEffect(() => {
if (!currentCoordinates || isReadOnly) return if (!currentCoordinates || isReadOnly) return
if ( if (
@ -142,15 +151,15 @@ const NonMemoizedDraggableGroupNode = ({
} }
useDrag( useDrag(
({ first, last, offset: [offsetX, offsetY], event, target }) => { ({ first, last, offset: [offsetX, offsetY], distance, event, target }) => {
event.stopPropagation() event.stopPropagation()
if ((target as HTMLElement).classList.contains('prevent-group-drag')) if ((target as HTMLElement).classList.contains('prevent-group-drag'))
return return
if (first) { if (first) {
setFocusedGroupId(group.id)
setIsMouseDown(true) setIsMouseDown(true)
} }
if (last) { if (last) {
if (distance[0] > 1 || distance[1] > 1) setIgnoreNextFocusIntent(true)
setIsMouseDown(false) setIsMouseDown(false)
} }
const newCoord = { const newCoord = {
@ -170,6 +179,14 @@ const NonMemoizedDraggableGroupNode = ({
} }
) )
const focusGroup = () => {
if (ignoreNextFocusIntent) {
setIgnoreNextFocusIntent(false)
return
}
setIsFocused(true)
}
return ( return (
<ContextMenu<HTMLDivElement> <ContextMenu<HTMLDivElement>
renderMenu={() => <GroupNodeContextMenu groupIndex={groupIndex} />} renderMenu={() => <GroupNodeContextMenu groupIndex={groupIndex} />}
@ -178,6 +195,7 @@ const NonMemoizedDraggableGroupNode = ({
{(ref, isContextMenuOpened) => ( {(ref, isContextMenuOpened) => (
<Stack <Stack
ref={setMultipleRefs([ref, groupRef])} ref={setMultipleRefs([ref, groupRef])}
onClick={focusGroup}
data-testid="group" data-testid="group"
p="4" p="4"
rounded="xl" rounded="xl"
@ -186,7 +204,7 @@ const NonMemoizedDraggableGroupNode = ({
isConnecting || isContextMenuOpened || isPreviewing ? '2px' : '1px' isConnecting || isContextMenuOpened || isPreviewing ? '2px' : '1px'
} }
borderColor={ borderColor={
isConnecting || isContextMenuOpened || isPreviewing isConnecting || isContextMenuOpened || isPreviewing || isFocused
? previewingBorderColor ? previewingBorderColor
: borderColor : borderColor
} }
@ -204,7 +222,7 @@ const NonMemoizedDraggableGroupNode = ({
cursor={isMouseDown ? 'grabbing' : 'pointer'} cursor={isMouseDown ? 'grabbing' : 'pointer'}
shadow="md" shadow="md"
_hover={{ shadow: 'lg' }} _hover={{ shadow: 'lg' }}
zIndex={focusedGroupId === group.id ? 10 : 1} zIndex={isFocused ? 10 : 1}
> >
<Editable <Editable
value={groupTitle} value={groupTitle}
@ -232,16 +250,24 @@ const NonMemoizedDraggableGroupNode = ({
isStartGroup={isStartGroup} isStartGroup={isStartGroup}
/> />
)} )}
<IconButton <SlideFade
icon={<PlayIcon />} in={isFocused}
aria-label={'Preview bot from this group'} style={{
pos="absolute" position: 'absolute',
right={2} top: '-50px',
top={0} right: 0,
size="sm" }}
variant="outline" unmountOnExit
onClick={startPreviewAtThisGroup} >
<GroupFocusToolbar
onPlayClick={startPreviewAtThisGroup}
onDuplicateClick={() => {
setIsFocused(false)
duplicateGroup(groupIndex)
}}
onDeleteClick={() => deleteGroup(groupIndex)}
/> />
</SlideFade>
</Stack> </Stack>
)} )}
</ContextMenu> </ContextMenu>

View File

@ -6,11 +6,13 @@ type Handler = (event: MouseEvent) => void
type Props<T> = { type Props<T> = {
ref: RefObject<T> ref: RefObject<T>
handler: Handler handler: Handler
capture?: boolean
} }
export const useOutsideClick = <T extends HTMLElement = HTMLElement>({ export const useOutsideClick = <T extends HTMLElement = HTMLElement>({
ref, ref,
handler, handler,
capture,
}: Props<T>): void => { }: Props<T>): void => {
const triggerHandlerIfOutside = (event: MouseEvent) => { const triggerHandlerIfOutside = (event: MouseEvent) => {
const el = ref?.current const el = ref?.current
@ -20,5 +22,7 @@ export const useOutsideClick = <T extends HTMLElement = HTMLElement>({
handler(event) handler(event)
} }
useEventListener('pointerdown', triggerHandlerIfOutside) useEventListener('pointerdown', triggerHandlerIfOutside, null, {
capture,
})
} }