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

View File

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