2
0

(editor) Improve graph pan when dragging on groups

This commit is contained in:
Baptiste Arnaud
2024-01-23 17:53:44 +01:00
parent 00dcb135f3
commit be74ad103a
9 changed files with 40 additions and 88 deletions

View File

@@ -1,68 +0,0 @@
import { MouseIcon, LaptopIcon } from '@/components/icons'
import { useTranslate } from '@tolgee/react'
import {
HStack,
Radio,
RadioGroup,
Stack,
VStack,
Text,
} from '@chakra-ui/react'
import { GraphNavigation } from '@typebot.io/prisma'
type Props = {
defaultValue: string
onChange: (value: string) => void
}
export const GraphNavigationRadioGroup = ({
defaultValue,
onChange,
}: Props) => {
const { t } = useTranslate()
const graphNavigationData = [
{
value: GraphNavigation.MOUSE,
label: t('account.preferences.graphNavigation.mouse.label'),
description: t('account.preferences.graphNavigation.mouse.description'),
icon: <MouseIcon boxSize="35px" />,
},
{
value: GraphNavigation.TRACKPAD,
label: t('account.preferences.graphNavigation.trackpad.label'),
description: t(
'account.preferences.graphNavigation.trackpad.description'
),
icon: <LaptopIcon boxSize="35px" />,
},
]
return (
<RadioGroup onChange={onChange} defaultValue={defaultValue}>
<HStack spacing={4} w="full" align="stretch">
{graphNavigationData.map((option) => (
<VStack
key={option.value}
as="label"
htmlFor={option.label}
cursor="pointer"
borderWidth="1px"
borderRadius="md"
w="full"
p="6"
spacing={6}
justifyContent="space-between"
>
<VStack spacing={6}>
{option.icon}
<Stack>
<Text fontWeight="bold">{option.label}</Text>
<Text>{option.description}</Text>
</Stack>
</VStack>
<Radio value={option.value} id={option.label} />
</VStack>
))}
</HStack>
</RadioGroup>
)
}

View File

@@ -11,7 +11,6 @@ import {
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { GraphNavigation } from '@typebot.io/prisma' import { GraphNavigation } from '@typebot.io/prisma'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { GraphNavigationRadioGroup } from './GraphNavigationRadioGroup'
import { AppearanceRadioGroup } from './AppearanceRadioGroup' import { AppearanceRadioGroup } from './AppearanceRadioGroup'
import { useUser } from '../hooks/useUser' import { useUser } from '../hooks/useUser'
import { ChevronDownIcon } from '@/components/icons' import { ChevronDownIcon } from '@/components/icons'
@@ -42,10 +41,6 @@ export const UserPreferencesForm = () => {
updateUser({ graphNavigation: GraphNavigation.TRACKPAD }) updateUser({ graphNavigation: GraphNavigation.TRACKPAD })
}, [updateUser, user?.graphNavigation]) }, [updateUser, user?.graphNavigation])
const changeGraphNavigation = async (value: string) => {
updateUser({ graphNavigation: value as GraphNavigation })
}
const changeAppearance = async (value: string) => { const changeAppearance = async (value: string) => {
updateUser({ preferredAppAppearance: value }) updateUser({ preferredAppAppearance: value })
} }
@@ -99,15 +94,7 @@ export const UserPreferencesForm = () => {
</MoreInfoTooltip> </MoreInfoTooltip>
)} )}
</HStack> </HStack>
<Stack spacing={6}>
<Heading size="md">
{t('account.preferences.graphNavigation.heading')}
</Heading>
<GraphNavigationRadioGroup
defaultValue={user?.graphNavigation ?? GraphNavigation.TRACKPAD}
onChange={changeGraphNavigation}
/>
</Stack>
<Stack spacing={6}> <Stack spacing={6}>
<Heading size="md"> <Heading size="md">
{t('account.preferences.appearance.heading')} {t('account.preferences.appearance.heading')}

View File

@@ -63,6 +63,8 @@ export const Graph = ({
setPreviewingEdge, setPreviewingEdge,
connectingIds, connectingIds,
} = useGraph() } = useGraph()
const isDraggingGraph = useGroupsStore((state) => state.isDraggingGraph)
const setIsDraggingGraph = useGroupsStore((state) => state.setIsDraggingGraph)
const focusedGroups = useGroupsStore( const focusedGroups = useGroupsStore(
useShallow((state) => state.focusedGroups) useShallow((state) => state.focusedGroups)
) )
@@ -107,7 +109,6 @@ export const Graph = ({
const [lastMouseClickPosition, setLastMouseClickPosition] = useState< const [lastMouseClickPosition, setLastMouseClickPosition] = useState<
Coordinates | undefined Coordinates | undefined
>() >()
const [isSpacePressed, setIsSpacePressed] = useState(false)
const [isDragging, setIsDragging] = useState(false) const [isDragging, setIsDragging] = useState(false)
const graphContainerRef = useRef<HTMLDivElement | null>(null) const graphContainerRef = useRef<HTMLDivElement | null>(null)
@@ -172,6 +173,7 @@ export const Graph = ({
} }
const handlePointerUp = (e: PointerEvent) => { const handlePointerUp = (e: PointerEvent) => {
if (isDraggingGraph) return
if ( if (
!selectBoxCoordinates || !selectBoxCoordinates ||
Math.abs(selectBoxCoordinates?.dimension.width) + Math.abs(selectBoxCoordinates?.dimension.width) +
@@ -192,7 +194,7 @@ export const Graph = ({
useGesture( useGesture(
{ {
onDrag: (props) => { onDrag: (props) => {
if (isSpacePressed) { if (isDraggingGraph) {
if (props.first) setIsDragging(true) if (props.first) setIsDragging(true)
if (props.last) setIsDragging(false) if (props.last) setIsDragging(false)
setGraphPosition({ setGraphPosition({
@@ -333,11 +335,11 @@ export const Graph = ({
}) })
useEventListener('keydown', (e) => { useEventListener('keydown', (e) => {
if (e.key === ' ') setIsSpacePressed(true) if (e.key === ' ') setIsDraggingGraph(true)
}) })
useEventListener('keyup', (e) => { useEventListener('keyup', (e) => {
if (e.key === ' ') { if (e.key === ' ') {
setIsSpacePressed(false) setIsDraggingGraph(false)
setIsDragging(false) setIsDragging(false)
} }
}) })
@@ -357,7 +359,7 @@ export const Graph = ({
const zoomIn = () => zoom({ delta: zoomButtonsScaleBlock }) const zoomIn = () => zoom({ delta: zoomButtonsScaleBlock })
const zoomOut = () => zoom({ delta: -zoomButtonsScaleBlock }) const zoomOut = () => zoom({ delta: -zoomButtonsScaleBlock })
const cursor = isSpacePressed ? (isDragging ? 'grabbing' : 'grab') : 'auto' const cursor = isDraggingGraph ? (isDragging ? 'grabbing' : 'grab') : 'auto'
return ( return (
<Flex <Flex

View File

@@ -44,6 +44,7 @@ import { TargetEndpoint } from '../../endpoints/TargetEndpoint'
import { SettingsModal } from './SettingsModal' import { SettingsModal } from './SettingsModal'
import { TElement } from '@udecode/plate-common' import { TElement } from '@udecode/plate-common'
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants' import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
import { useGroupsStore } from '@/features/graph/hooks/useGroupsStore'
export const BlockNode = ({ export const BlockNode = ({
block, block,
@@ -88,6 +89,8 @@ export const BlockNode = ({
const groupId = typebot?.groups[indices.groupIndex].id const groupId = typebot?.groups[indices.groupIndex].id
const isDraggingGraph = useGroupsStore((state) => state.isDraggingGraph)
const onDrag = (position: NodePosition) => { const onDrag = (position: NodePosition) => {
if (!onMouseDown) return if (!onMouseDown) return
onMouseDown(position, block) onMouseDown(position, block)
@@ -212,7 +215,7 @@ export const BlockNode = ({
data-testid={`block ${block.id}`} data-testid={`block ${block.id}`}
w="full" w="full"
className="prevent-group-drag" className="prevent-group-drag"
pointerEvents={isReadOnly ? 'none' : 'auto'} pointerEvents={isReadOnly || isDraggingGraph ? 'none' : 'auto'}
> >
<HStack <HStack
flex="1" flex="1"

View File

@@ -19,6 +19,7 @@ import { EventNodeContent } from './EventNodeContent'
import { EventSourceEndpoint } from '../../endpoints/EventSourceEndpoint' import { EventSourceEndpoint } from '../../endpoints/EventSourceEndpoint'
import { eventWidth } from '@/features/graph/constants' import { eventWidth } from '@/features/graph/constants'
import { useTypebot } from '@/features/editor/providers/TypebotProvider' import { useTypebot } from '@/features/editor/providers/TypebotProvider'
import { useGroupsStore } from '@/features/graph/hooks/useGroupsStore'
type Props = { type Props = {
event: TEvent event: TEvent
@@ -70,6 +71,7 @@ const NonMemoizedDraggableEventNode = ({
const eventRef = useRef<HTMLDivElement | null>(null) const eventRef = useRef<HTMLDivElement | null>(null)
const [debouncedEventPosition] = useDebounce(currentCoordinates, 100) const [debouncedEventPosition] = useDebounce(currentCoordinates, 100)
const [isFocused, setIsFocused] = useState(false) const [isFocused, setIsFocused] = useState(false)
const isDraggingGraph = useGroupsStore((state) => state.isDraggingGraph)
useOutsideClick({ useOutsideClick({
handler: () => setIsFocused(false), handler: () => setIsFocused(false),
@@ -172,6 +174,7 @@ const NonMemoizedDraggableEventNode = ({
shadow="md" shadow="md"
_hover={{ shadow: 'lg' }} _hover={{ shadow: 'lg' }}
zIndex={isFocused ? 10 : 1} zIndex={isFocused ? 10 : 1}
pointerEvents={isDraggingGraph ? 'none' : 'auto'}
> >
<EventNodeContent event={event} /> <EventNodeContent event={event} />
<EventSourceEndpoint <EventSourceEndpoint

View File

@@ -67,6 +67,7 @@ export const GroupNode = ({ group, groupIndex }: Props) => {
isNotDefined(previewingEdge.to.blockId)))) isNotDefined(previewingEdge.to.blockId))))
const groupRef = useRef<HTMLDivElement | null>(null) const groupRef = useRef<HTMLDivElement | null>(null)
const isDraggingGraph = useGroupsStore((state) => state.isDraggingGraph)
const focusedGroups = useGroupsStore( const focusedGroups = useGroupsStore(
useShallow((state) => state.focusedGroups) useShallow((state) => state.focusedGroups)
) )
@@ -192,6 +193,7 @@ export const GroupNode = ({ group, groupIndex }: Props) => {
_hover={{ shadow: 'lg' }} _hover={{ shadow: 'lg' }}
zIndex={isFocused ? 10 : 1} zIndex={isFocused ? 10 : 1}
spacing={isEmpty(group.title) ? '0' : '2'} spacing={isEmpty(group.title) ? '0' : '2'}
pointerEvents={isDraggingGraph ? 'none' : 'auto'}
> >
<Editable <Editable
value={groupTitle} value={groupTitle}

View File

@@ -6,6 +6,7 @@ type Store = {
focusedGroups: string[] focusedGroups: string[]
groupsCoordinates: CoordinatesMap | undefined groupsCoordinates: CoordinatesMap | undefined
groupsInClipboard: { groups: GroupV6[]; edges: Edge[] } | undefined groupsInClipboard: { groups: GroupV6[]; edges: Edge[] } | undefined
isDraggingGraph: boolean
// TO-DO: remove once Typebot provider is migrated to a Zustand store. We will be able to get it internally in the store (if mutualized). // TO-DO: remove once Typebot provider is migrated to a Zustand store. We will be able to get it internally in the store (if mutualized).
getGroupsCoordinates: () => CoordinatesMap | undefined getGroupsCoordinates: () => CoordinatesMap | undefined
focusGroup: (groupId: string, isAppending?: boolean) => void focusGroup: (groupId: string, isAppending?: boolean) => void
@@ -15,12 +16,14 @@ type Store = {
setGroupsCoordinates: (groups: Group[] | undefined) => void setGroupsCoordinates: (groups: Group[] | undefined) => void
updateGroupCoordinates: (groupId: string, newCoord: Coordinates) => void updateGroupCoordinates: (groupId: string, newCoord: Coordinates) => void
copyGroups: (groups: GroupV6[], edges: Edge[]) => void copyGroups: (groups: GroupV6[], edges: Edge[]) => void
setIsDraggingGraph: (isDragging: boolean) => void
} }
export const useGroupsStore = createWithEqualityFn<Store>((set, get) => ({ export const useGroupsStore = createWithEqualityFn<Store>((set, get) => ({
focusedGroups: [], focusedGroups: [],
groupsCoordinates: undefined, groupsCoordinates: undefined,
groupsInClipboard: undefined, groupsInClipboard: undefined,
isDraggingGraph: false,
getGroupsCoordinates: () => get().groupsCoordinates, getGroupsCoordinates: () => get().groupsCoordinates,
focusGroup: (groupId, isShiftKeyPressed) => focusGroup: (groupId, isShiftKeyPressed) =>
set((state) => ({ set((state) => ({
@@ -80,4 +83,5 @@ export const useGroupsStore = createWithEqualityFn<Store>((set, get) => ({
edges, edges,
}, },
}), }),
setIsDraggingGraph: (isDragging) => set({ isDraggingGraph: isDragging }),
})) }))

View File

@@ -0,0 +1,18 @@
---
title: Graph
icon: game-board
---
import { LoomVideo } from '/snippets/loom-video.mdx'
The Graph is where you arrange your conversation flow and connect the Typebot [blocks](./blocks/overview) together.
## Gestures
**Select**: `Left click` + `drag`
**Zoom**: `Ctrl` + `Mouse wheel` on a mouse or `pinch` on a trackpad
**Pan**: `Space` + `Mouse wheel` on a mouse or `two-finger drag` on a trackpad
<LoomVideo id="200d88212d44421fb713a7623e222c9b" />

View File

@@ -76,6 +76,7 @@
{ {
"group": "Flow", "group": "Flow",
"pages": [ "pages": [
"editor/graph",
{ {
"group": "Blocks", "group": "Blocks",
"icon": "block", "icon": "block",