🚸 (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.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()
|
||||
|
@ -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,
|
||||
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>
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user