🚸 (editor) Show toolbar on group click
This commit is contained in:
@ -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()
|
||||||
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user