2
0

(editor) Improve edges responsiveness

Also fixed a ton of useEffects should make everything a bit more reactive.

Closes #307
This commit is contained in:
Baptiste Arnaud
2023-02-28 15:06:43 +01:00
parent caf4086dd8
commit f8f98adc1c
24 changed files with 438 additions and 429 deletions

View File

@ -20,9 +20,11 @@ const authenticateByToken = async (
apiToken: string apiToken: string
): Promise<User | undefined> => { ): Promise<User | undefined> => {
if (typeof window !== 'undefined') return if (typeof window !== 'undefined') return
return (await prisma.user.findFirst({ const user = (await prisma.user.findFirst({
where: { apiTokens: { some: { token: apiToken } } }, where: { apiTokens: { some: { token: apiToken } } },
})) as User })) as User
setUser({ id: user.id, email: user.email ?? undefined })
return user
} }
const extractBearerToken = (req: NextApiRequest) => const extractBearerToken = (req: NextApiRequest) =>

View File

@ -139,7 +139,6 @@ test('plan changes should work', async ({ page }) => {
await expect(page.locator('text=$73.00 >> nth=0')).toBeVisible() await expect(page.locator('text=$73.00 >> nth=0')).toBeVisible()
await expect(page.locator('text=$30.00 >> nth=0')).toBeVisible() await expect(page.locator('text=$30.00 >> nth=0')).toBeVisible()
await expect(page.locator('text=$4.00 >> nth=0')).toBeVisible() await expect(page.locator('text=$4.00 >> nth=0')).toBeVisible()
await expect(page.locator('text=user@email.com')).toBeVisible()
await addSubscriptionToWorkspace( await addSubscriptionToWorkspace(
planChangeWorkspaceId, planChangeWorkspaceId,
[ [

View File

@ -162,6 +162,7 @@ export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
</Stack> </Stack>
<SwitchWithLabel <SwitchWithLabel
label={'Custom content?'} label={'Custom content?'}
moreInfoContent="By default, the email body will be a recap of what has been collected so far. You can override it with this option."
initialValue={options.isCustomBody ?? false} initialValue={options.isCustomBody ?? false}
onCheckChange={handleIsCustomBodyChange} onCheckChange={handleIsCustomBodyChange}
/> />

View File

@ -44,7 +44,7 @@ export const EditorPage = () => {
{!isReadOnly && <BlocksSideBar />} {!isReadOnly && <BlocksSideBar />}
<GraphProvider isReadOnly={isReadOnly}> <GraphProvider isReadOnly={isReadOnly}>
<GroupsCoordinatesProvider groups={typebot.groups}> <GroupsCoordinatesProvider groups={typebot.groups}>
<Graph flex="1" typebot={typebot} /> <Graph flex="1" typebot={typebot} key={typebot.id} />
<BoardMenuButton pos="absolute" right="40px" top="20px" /> <BoardMenuButton pos="absolute" right="40px" top="20px" />
<RightPanel /> <RightPanel />
</GroupsCoordinatesProvider> </GroupsCoordinatesProvider>

View File

@ -1,152 +1,108 @@
import { isDefined } from '@udecode/plate-core' import { isDefined } from '@udecode/plate-core'
import { dequal } from 'dequal' import { dequal } from 'dequal'
// import { diff } from 'deep-object-diff' import { useCallback, useRef, useState } from 'react'
import { useReducer, useCallback, useRef } from 'react'
import { isNotDefined } from 'utils'
enum ActionType { export interface Actions<T extends { updatedAt: Date }> {
Undo = 'UNDO', set: (newPresent: T | ((current: T) => T) | undefined) => void
Redo = 'REDO',
Set = 'SET',
Flush = 'FLUSH',
}
export interface Actions<T extends { updatedAt: Date } | undefined> {
set: (
newPresent: T | ((current: T) => T),
options?: { updateDate: boolean }
) => void
undo: () => void undo: () => void
redo: () => void redo: () => void
flush: () => void flush: () => void
canUndo: boolean canUndo: boolean
canRedo: boolean canRedo: boolean
presentRef: React.MutableRefObject<T>
} }
interface Action<T extends { updatedAt: Date } | undefined> { export interface History<T extends { updatedAt: Date }> {
type: ActionType
newPresent?: T
updateDate?: boolean
}
export interface State<T extends { updatedAt: Date } | undefined> {
past: T[] past: T[]
present: T present: T | undefined
future: T[] future: T[]
} }
const initialState = { const initialState = {
past: [], past: [],
present: null, present: undefined,
future: [], future: [],
} }
const reducer = <T extends { updatedAt: Date } | undefined>( export const useUndo = <T extends { updatedAt: Date }>(
state: State<T>, initialPresent?: T
action: Action<T> ): [T | undefined, Actions<T>] => {
) => { const [history, setHistory] = useState<History<T>>(initialState)
const { past, present, future } = state const presentRef = useRef<T | null>(initialPresent ?? null)
switch (action.type) { const canUndo = history.past.length !== 0
case ActionType.Undo: { const canRedo = history.future.length !== 0
if (past.length === 0 || !present) {
return state
}
const previous = past[past.length - 1] const undo = useCallback(() => {
const newPast = past.slice(0, past.length - 1) const { past, present, future } = history
if (past.length === 0 || !present) return
return { const previous = past[past.length - 1]
past: newPast, const newPast = past.slice(0, past.length - 1)
present: { ...previous, updatedAt: present.updatedAt },
future: [present, ...future],
}
}
case ActionType.Redo: { const newPresent = { ...previous, updatedAt: present.updatedAt }
if (future.length === 0) {
return state
}
const next = future[0]
const newFuture = future.slice(1)
return { setHistory({
past: [...past, present], past: newPast,
present: next, present: newPresent,
future: newFuture, future: [present, ...future],
} })
} presentRef.current = newPresent
}, [history])
case ActionType.Set: { const redo = useCallback(() => {
const { newPresent } = action const { past, present, future } = history
if (future.length === 0) return
const next = future[0]
const newFuture = future.slice(1)
setHistory({
past: present ? [...past, present] : past,
present: next,
future: newFuture,
})
presentRef.current = next
}, [history])
const set = useCallback(
(newPresentArg: T | ((current: T) => T) | undefined) => {
const { past, present } = history
const newPresent =
typeof newPresentArg === 'function'
? newPresentArg(presentRef.current as T)
: newPresentArg
if ( if (
isNotDefined(newPresent) || newPresent &&
(present && present &&
dequal( dequal(
JSON.parse(JSON.stringify(newPresent)), JSON.parse(JSON.stringify(newPresent)),
JSON.parse(JSON.stringify(present)) JSON.parse(JSON.stringify(present))
)) )
) { ) {
return state return
} }
return { if (newPresent === undefined) {
presentRef.current = null
setHistory(initialState)
return
}
setHistory({
past: [...past, present].filter(isDefined), past: [...past, present].filter(isDefined),
present: newPresent, present: newPresent,
future: [], future: [],
} })
} presentRef.current = newPresent
},
[history]
)
case ActionType.Flush: const flush = useCallback(() => {
return { ...initialState, present } setHistory({
} present: presentRef.current ?? undefined,
} past: [],
future: [],
const useUndo = <T extends { updatedAt: Date } | undefined>(
initialPresent: T
): [State<T>, Actions<T>] => {
const [state, dispatch] = useReducer(reducer, {
...initialState,
present: initialPresent,
})
const presentRef = useRef<T>(initialPresent)
const canUndo = state.past.length !== 0
const canRedo = state.future.length !== 0
const undo = useCallback(() => {
if (canUndo) {
dispatch({ type: ActionType.Undo })
}
}, [canUndo])
const redo = useCallback(() => {
if (canRedo) {
dispatch({ type: ActionType.Redo })
}
}, [canRedo])
const set = useCallback((newPresent: T | ((current: T) => T)) => {
const updatedTypebot =
newPresent && typeof newPresent === 'function'
? newPresent(presentRef.current)
: newPresent
presentRef.current = updatedTypebot
dispatch({
type: ActionType.Set,
newPresent: updatedTypebot,
}) })
}, []) }, [])
const flush = useCallback(() => { return [history.present, { set, undo, redo, flush, canUndo, canRedo }]
dispatch({ type: ActionType.Flush })
}, [])
return [
state as State<T>,
{ set, undo, redo, flush, canUndo, canRedo, presentRef },
]
} }
export default useUndo

View File

@ -26,7 +26,7 @@ import { variablesAction, VariablesActions } from './actions/variables'
import { dequal } from 'dequal' import { dequal } from 'dequal'
import { useToast } from '@/hooks/useToast' import { useToast } from '@/hooks/useToast'
import { useTypebotQuery } from '@/hooks/useTypebotQuery' import { useTypebotQuery } from '@/hooks/useTypebotQuery'
import useUndo from '../../hooks/useUndo' import { useUndo } from '../../hooks/useUndo'
import { useLinkedTypebots } from '@/hooks/useLinkedTypebots' import { useLinkedTypebots } from '@/hooks/useLinkedTypebots'
import { updateTypebotQuery } from '../../queries/updateTypebotQuery' import { updateTypebotQuery } from '../../queries/updateTypebotQuery'
import { preventUserFromRefreshing } from '@/utils/helpers' import { preventUserFromRefreshing } from '@/utils/helpers'
@ -46,7 +46,6 @@ import {
import { useAutoSave } from '@/hooks/useAutoSave' import { useAutoSave } from '@/hooks/useAutoSave'
import { createWebhookQuery } from '@/features/blocks/integrations/webhook/queries/createWebhookQuery' import { createWebhookQuery } from '@/features/blocks/integrations/webhook/queries/createWebhookQuery'
import { duplicateWebhookQuery } from '@/features/blocks/integrations/webhook/queries/duplicateWebhookQuery' import { duplicateWebhookQuery } from '@/features/blocks/integrations/webhook/queries/duplicateWebhookQuery'
import { useSession } from 'next-auth/react'
const autoSaveTimeout = 10000 const autoSaveTimeout = 10000
@ -104,27 +103,24 @@ export const TypebotProvider = ({
children: ReactNode children: ReactNode
typebotId?: string typebotId?: string
}) => { }) => {
const { status } = useSession()
const { push } = useRouter() const { push } = useRouter()
const { showToast } = useToast() const { showToast } = useToast()
const { typebot, publishedTypebot, webhooks, isReadOnly, isLoading, mutate } = const {
useTypebotQuery({ typebot,
typebotId, publishedTypebot,
}) webhooks,
isReadOnly,
isLoading: isFetchingTypebot,
mutate,
} = useTypebotQuery({
typebotId,
})
const [ const [
{ present: localTypebot }, localTypebot,
{ { redo, undo, flush, canRedo, canUndo, set: setLocalTypebot },
redo, ] = useUndo<Typebot>(undefined)
undo,
flush,
canRedo,
canUndo,
set: setLocalTypebot,
presentRef: currentTypebotRef,
},
] = useUndo<Typebot | undefined>(undefined)
const linkedTypebotIds = const linkedTypebotIds =
localTypebot?.groups localTypebot?.groups
@ -151,23 +147,34 @@ export const TypebotProvider = ({
}) })
useEffect(() => { useEffect(() => {
if (!typebot || !currentTypebotRef.current) return if (!typebot && isDefined(localTypebot)) setLocalTypebot(undefined)
if (typebotId !== currentTypebotRef.current.id) { if (isFetchingTypebot) return
setLocalTypebot({ ...typebot }, { updateDate: false }) if (!typebot) {
flush() showToast({ status: 'info', description: "Couldn't find typebot" })
} else if ( push('/typebots')
return
}
if (
typebot.id !== localTypebot?.id ||
new Date(typebot.updatedAt).getTime() > new Date(typebot.updatedAt).getTime() >
new Date(currentTypebotRef.current.updatedAt).getTime() new Date(localTypebot.updatedAt).getTime()
) { ) {
setLocalTypebot({ ...typebot }) setLocalTypebot({ ...typebot })
flush()
} }
}, [
flush,
isFetchingTypebot,
localTypebot,
push,
setLocalTypebot,
showToast,
typebot,
])
// eslint-disable-next-line react-hooks/exhaustive-deps const saveTypebot = useCallback(async () => {
}, [typebot]) if (!localTypebot || !typebot) return
const typebotToSave = { ...localTypebot }
const saveTypebot = async () => {
if (!currentTypebotRef.current || !typebot) return
const typebotToSave = { ...currentTypebotRef.current }
if (dequal(omit(typebot, 'updatedAt'), omit(typebotToSave, 'updatedAt'))) if (dequal(omit(typebot, 'updatedAt'), omit(typebotToSave, 'updatedAt')))
return return
setIsSavingLoading(true) setIsSavingLoading(true)
@ -187,7 +194,15 @@ export const TypebotProvider = ({
webhooks: webhooks ?? [], webhooks: webhooks ?? [],
}) })
window.removeEventListener('beforeunload', preventUserFromRefreshing) window.removeEventListener('beforeunload', preventUserFromRefreshing)
} }, [
localTypebot,
mutate,
publishedTypebot,
setLocalTypebot,
showToast,
typebot,
webhooks,
])
const savePublishedTypebot = async (newPublishedTypebot: PublicTypebot) => { const savePublishedTypebot = async (newPublishedTypebot: PublicTypebot) => {
if (!localTypebot) return if (!localTypebot) return
@ -201,7 +216,7 @@ export const TypebotProvider = ({
if (error) if (error)
return showToast({ title: error.name, description: error.message }) return showToast({ title: error.name, description: error.message })
mutate({ mutate({
typebot: currentTypebotRef.current as Typebot, typebot: localTypebot,
publishedTypebot: newPublishedTypebot, publishedTypebot: newPublishedTypebot,
webhooks: webhooks ?? [], webhooks: webhooks ?? [],
}) })
@ -213,7 +228,7 @@ export const TypebotProvider = ({
item: localTypebot, item: localTypebot,
debounceTimeout: autoSaveTimeout, debounceTimeout: autoSaveTimeout,
}, },
[typebot, publishedTypebot, webhooks] [saveTypebot, localTypebot]
) )
useEffect(() => { useEffect(() => {
@ -221,8 +236,7 @@ export const TypebotProvider = ({
return () => { return () => {
Router.events.off('routeChangeStart', saveTypebot) Router.events.off('routeChangeStart', saveTypebot)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [saveTypebot])
}, [typebot, publishedTypebot, webhooks])
const [isSavingLoading, setIsSavingLoading] = useState(false) const [isSavingLoading, setIsSavingLoading] = useState(false)
const [isPublishing, setIsPublishing] = useState(false) const [isPublishing, setIsPublishing] = useState(false)
@ -237,26 +251,14 @@ export const TypebotProvider = ({
useEffect(() => { useEffect(() => {
if (!localTypebot || !typebot) return if (!localTypebot || !typebot) return
currentTypebotRef.current = localTypebot
if (!checkIfTypebotsAreEqual(localTypebot, typebot)) { if (!checkIfTypebotsAreEqual(localTypebot, typebot)) {
window.removeEventListener('beforeunload', preventUserFromRefreshing)
window.addEventListener('beforeunload', preventUserFromRefreshing) window.addEventListener('beforeunload', preventUserFromRefreshing)
} else { }
return () => {
window.removeEventListener('beforeunload', preventUserFromRefreshing) window.removeEventListener('beforeunload', preventUserFromRefreshing)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [localTypebot, typebot])
}, [localTypebot])
useEffect(() => {
if (status !== 'authenticated' || isLoading) return
if (!typebot) {
showToast({ status: 'info', description: "Couldn't find typebot" })
push('/typebots')
return
}
setLocalTypebot({ ...typebot })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [status, isLoading])
const updateLocalTypebot = (updates: UpdateTypebotPayload) => const updateLocalTypebot = (updates: UpdateTypebotPayload) =>
localTypebot && setLocalTypebot({ ...localTypebot, ...updates }) localTypebot && setLocalTypebot({ ...localTypebot, ...updates })

View File

@ -34,8 +34,10 @@ export const edgesAction = (setTypebot: SetTypebot): EdgesActions => ({
) )
const itemIndex = edge.from.itemId const itemIndex = edge.from.itemId
? ( ? (
typebot.groups[groupIndex].blocks[blockIndex] as BlockWithItems typebot.groups[groupIndex].blocks[blockIndex] as
).items.findIndex(byId(edge.from.itemId)) | BlockWithItems
| undefined
)?.items.findIndex(byId(edge.from.itemId))
: null : null
isDefined(itemIndex) && itemIndex !== -1 isDefined(itemIndex) && itemIndex !== -1

View File

@ -9,20 +9,15 @@ import {
import { useTypebot } from '@/features/editor' import { useTypebot } from '@/features/editor'
import { colors } from '@/lib/theme' import { colors } from '@/lib/theme'
import React, { useMemo, useState } from 'react' import React, { useMemo, useState } from 'react'
import { import { computeConnectingEdgePath, computeEdgePathToMouse } from '../../utils'
computeConnectingEdgePath, import { useEndpoints } from '../../providers/EndpointsProvider'
computeEdgePathToMouse,
getEndpointTopOffset,
} from '../../utils'
export const DrawingEdge = () => { export const DrawingEdge = () => {
const { graphPosition, setConnectingIds, connectingIds } = useGraph()
const { const {
graphPosition, sourceEndpointYOffsets: sourceEndpoints,
setConnectingIds, targetEndpointYOffsets: targetEndpoints,
connectingIds, } = useEndpoints()
sourceEndpoints,
targetEndpoints,
} = useGraph()
const { groupsCoordinates } = useGroupsCoordinates() const { groupsCoordinates } = useGroupsCoordinates()
const { createEdge } = useTypebot() const { createEdge } = useTypebot()
const [mousePosition, setMousePosition] = useState<Coordinates | null>(null) const [mousePosition, setMousePosition] = useState<Coordinates | null>(null)
@ -34,24 +29,15 @@ export const DrawingEdge = () => {
const sourceTop = useMemo(() => { const sourceTop = useMemo(() => {
if (!connectingIds) return 0 if (!connectingIds) return 0
return getEndpointTopOffset({ const endpointId =
endpoints: sourceEndpoints, connectingIds.source.itemId ?? connectingIds.source.blockId
graphOffsetY: graphPosition.y, return sourceEndpoints.get(endpointId)?.y
endpointId: connectingIds.source.itemId ?? connectingIds.source.blockId,
graphScale: graphPosition.scale,
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [connectingIds, sourceEndpoints]) }, [connectingIds, sourceEndpoints])
const targetTop = useMemo(() => { const targetTop = useMemo(() => {
if (!connectingIds) return 0 if (!connectingIds) return 0
return getEndpointTopOffset({ const endpointId = connectingIds.target?.blockId
endpoints: targetEndpoints, return endpointId ? targetEndpoints.get(endpointId)?.y : undefined
graphOffsetY: graphPosition.y,
endpointId: connectingIds.target?.blockId,
graphScale: graphPosition.scale,
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [connectingIds, targetEndpoints]) }, [connectingIds, targetEndpoints])
const path = useMemo(() => { const path = useMemo(() => {

View File

@ -6,18 +6,15 @@ import {
useColorModeValue, useColorModeValue,
theme, theme,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { useGraph, useGroupsCoordinates } from '../../providers' import { useGroupsCoordinates } from '../../providers'
import { useTypebot } from '@/features/editor' import { useTypebot } from '@/features/editor'
import { useWorkspace } from '@/features/workspace' import { useWorkspace } from '@/features/workspace'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { byId, isDefined } from 'utils' import { byId, isDefined } from 'utils'
import { isProPlan } from '@/features/billing' import { isProPlan } from '@/features/billing'
import { AnswersCount } from '@/features/analytics' import { AnswersCount } from '@/features/analytics'
import { import { computeSourceCoordinates, computeDropOffPath } from '../../utils'
getEndpointTopOffset, import { useEndpoints } from '../../providers/EndpointsProvider'
computeSourceCoordinates,
computeDropOffPath,
} from '../../utils'
type Props = { type Props = {
groupId: string groupId: string
@ -36,7 +33,7 @@ export const DropOffEdge = ({
) )
const { workspace } = useWorkspace() const { workspace } = useWorkspace()
const { groupsCoordinates } = useGroupsCoordinates() const { groupsCoordinates } = useGroupsCoordinates()
const { sourceEndpoints, graphPosition } = useGraph() const { sourceEndpointYOffsets: sourceEndpoints } = useEndpoints()
const { publishedTypebot } = useTypebot() const { publishedTypebot } = useTypebot()
const isWorkspaceProPlan = isProPlan(workspace) const isWorkspaceProPlan = isProPlan(workspace)
@ -68,17 +65,11 @@ export const DropOffEdge = ({
}, [answersCounts, groupId, totalAnswers, publishedTypebot]) }, [answersCounts, groupId, totalAnswers, publishedTypebot])
const group = publishedTypebot?.groups.find(byId(groupId)) const group = publishedTypebot?.groups.find(byId(groupId))
const sourceTop = useMemo(
() => const sourceTop = useMemo(() => {
getEndpointTopOffset({ const endpointId = group?.blocks[group.blocks.length - 1].id
endpoints: sourceEndpoints, return endpointId ? sourceEndpoints.get(endpointId)?.y : undefined
graphOffsetY: graphPosition.y, }, [group?.blocks, sourceEndpoints])
endpointId: group?.blocks[group.blocks.length - 1].id,
graphScale: graphPosition.scale,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[group?.blocks, sourceEndpoints, groupsCoordinates]
)
const labelCoordinates = useMemo(() => { const labelCoordinates = useMemo(() => {
if (!groupsCoordinates[groupId]) return if (!groupsCoordinates[groupId]) return

View File

@ -1,16 +1,12 @@
import { Coordinates, useGraph, useGroupsCoordinates } from '../../providers' import { Coordinates, useGraph, useGroupsCoordinates } from '../../providers'
import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react' import React, { useMemo, useState } from 'react'
import { Edge as EdgeProps } from 'models' import { Edge as EdgeProps } from 'models'
import { Portal, useColorMode, useDisclosure } from '@chakra-ui/react' import { Portal, useColorMode, useDisclosure } from '@chakra-ui/react'
import { useTypebot } from '@/features/editor' import { useTypebot } from '@/features/editor'
import { EdgeMenu } from './EdgeMenu' import { EdgeMenu } from './EdgeMenu'
import { colors } from '@/lib/theme' import { colors } from '@/lib/theme'
import { import { getAnchorsPosition, computeEdgePath } from '../../utils'
getEndpointTopOffset, import { useEndpoints } from '../../providers/EndpointsProvider'
getSourceEndpointId,
getAnchorsPosition,
computeEdgePath,
} from '../../utils'
export type AnchorsPositionProps = { export type AnchorsPositionProps = {
sourcePosition: Coordinates sourcePosition: Coordinates
@ -22,22 +18,17 @@ export type AnchorsPositionProps = {
type Props = { type Props = {
edge: EdgeProps edge: EdgeProps
} }
export const Edge = ({ edge }: Props) => { export const Edge = ({ edge }: Props) => {
const isDark = useColorMode().colorMode === 'dark' const isDark = useColorMode().colorMode === 'dark'
const { deleteEdge } = useTypebot() const { deleteEdge } = useTypebot()
const { const { previewingEdge, graphPosition, isReadOnly, setPreviewingEdge } =
previewingEdge, useGraph()
sourceEndpoints, const { sourceEndpointYOffsets, targetEndpointYOffsets } = useEndpoints()
targetEndpoints,
graphPosition,
isReadOnly,
setPreviewingEdge,
} = useGraph()
const { groupsCoordinates } = useGroupsCoordinates() const { groupsCoordinates } = useGroupsCoordinates()
const [isMouseOver, setIsMouseOver] = useState(false) const [isMouseOver, setIsMouseOver] = useState(false)
const { isOpen, onOpen, onClose } = useDisclosure() const { isOpen, onOpen, onClose } = useDisclosure()
const [edgeMenuPosition, setEdgeMenuPosition] = useState({ x: 0, y: 0 }) const [edgeMenuPosition, setEdgeMenuPosition] = useState({ x: 0, y: 0 })
const [refreshEdge, setRefreshEdge] = useState(false)
const isPreviewing = isMouseOver || previewingEdge?.id === edge.id const isPreviewing = isMouseOver || previewingEdge?.id === edge.id
@ -46,47 +37,20 @@ export const Edge = ({ edge }: Props) => {
const targetGroupCoordinates = const targetGroupCoordinates =
groupsCoordinates && groupsCoordinates[edge.to.groupId] groupsCoordinates && groupsCoordinates[edge.to.groupId]
const sourceTop = useMemo( const sourceTop = useMemo(() => {
const endpointId = edge?.from.itemId ?? edge?.from.blockId
if (!endpointId) return
return sourceEndpointYOffsets.get(endpointId)?.y
}, [edge?.from.itemId, edge?.from.blockId, sourceEndpointYOffsets])
const targetTop = useMemo(
() => () =>
getEndpointTopOffset({ edge?.to.blockId
endpoints: sourceEndpoints, ? targetEndpointYOffsets.get(edge?.to.blockId)?.y
graphOffsetY: graphPosition.y, : undefined,
endpointId: getSourceEndpointId(edge), [edge?.to.blockId, targetEndpointYOffsets]
graphScale: graphPosition.scale,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[sourceGroupCoordinates?.y, edge, sourceEndpoints, refreshEdge]
) )
useEffect(() => {
setTimeout(() => setRefreshEdge(true), 50)
}, [])
const [targetTop, setTargetTop] = useState(
getEndpointTopOffset({
endpoints: targetEndpoints,
graphOffsetY: graphPosition.y,
endpointId: edge?.to.blockId,
graphScale: graphPosition.scale,
})
)
useLayoutEffect(() => {
setTargetTop(
getEndpointTopOffset({
endpoints: targetEndpoints,
graphOffsetY: graphPosition.y,
endpointId: edge?.to.blockId,
graphScale: graphPosition.scale,
})
)
}, [
targetGroupCoordinates?.y,
targetEndpoints,
graphPosition.y,
edge?.to.blockId,
graphPosition.scale,
])
const path = useMemo(() => { const path = useMemo(() => {
if (!sourceGroupCoordinates || !targetGroupCoordinates || !sourceTop) if (!sourceGroupCoordinates || !targetGroupCoordinates || !sourceTop)
return `` return ``
@ -98,13 +62,12 @@ export const Edge = ({ edge }: Props) => {
graphScale: graphPosition.scale, graphScale: graphPosition.scale,
}) })
return computeEdgePath(anchorsPosition) return computeEdgePath(anchorsPosition)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ }, [
sourceGroupCoordinates?.x, sourceGroupCoordinates,
sourceGroupCoordinates?.y, targetGroupCoordinates,
targetGroupCoordinates?.x,
targetGroupCoordinates?.y,
sourceTop, sourceTop,
targetTop,
graphPosition.scale,
]) ])
const handleMouseEnter = () => setIsMouseOver(true) const handleMouseEnter = () => setIsMouseOver(true)

View File

@ -6,7 +6,16 @@ import {
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { useGraph, useGroupsCoordinates } from '../../providers' import { useGraph, useGroupsCoordinates } from '../../providers'
import { Source } from 'models' import { Source } from 'models'
import React, { useEffect, useRef, useState } from 'react' import React, {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useEndpoints } from '../../providers/EndpointsProvider'
const endpointHeight = 32
export const SourceEndpoint = ({ export const SourceEndpoint = ({
source, source,
@ -17,23 +26,61 @@ export const SourceEndpoint = ({
const color = useColorModeValue('blue.200', 'blue.100') const color = useColorModeValue('blue.200', 'blue.100')
const connectedColor = useColorModeValue('blue.300', 'blue.200') const connectedColor = useColorModeValue('blue.300', 'blue.200')
const bg = useColorModeValue('gray.100', 'gray.700') const bg = useColorModeValue('gray.100', 'gray.700')
const [ranOnce, setRanOnce] = useState(false) const { setConnectingIds, previewingEdge, graphPosition } = useGraph()
const { setConnectingIds, addSourceEndpoint, previewingEdge } = useGraph() const { setSourceEndpointYOffset: addSourceEndpoint } = useEndpoints()
const { groupsCoordinates } = useGroupsCoordinates() const { groupsCoordinates } = useGroupsCoordinates()
const ref = useRef<HTMLDivElement | null>(null) const ref = useRef<HTMLDivElement | null>(null)
const [groupHeight, setGroupHeight] = useState<number>()
const [groupTransformProp, setGroupTransformProp] = useState<string>()
const endpointY = useMemo(
() =>
ref.current
? (ref.current?.getBoundingClientRect().y +
(endpointHeight * graphPosition.scale) / 2 -
graphPosition.y) /
graphPosition.scale
: undefined,
// We need to force recompute whenever the group height and position changes
// eslint-disable-next-line react-hooks/exhaustive-deps
[graphPosition.scale, graphPosition.y, groupHeight, groupTransformProp]
)
useLayoutEffect(() => {
const resizeObserver = new ResizeObserver((entries) => {
setGroupHeight(entries[0].contentRect.height)
})
const groupElement = document.getElementById(`group-${source.groupId}`)
if (!groupElement) return
resizeObserver.observe(groupElement)
return () => {
resizeObserver.disconnect()
}
}, [source.groupId])
useLayoutEffect(() => {
const mutationObserver = new MutationObserver((entries) => {
setGroupTransformProp((entries[0].target as HTMLElement).style.transform)
})
const groupElement = document.getElementById(`group-${source.groupId}`)
if (!groupElement) return
mutationObserver.observe(groupElement, {
attributes: true,
attributeFilter: ['style'],
})
return () => {
mutationObserver.disconnect()
}
}, [source.groupId])
useEffect(() => { useEffect(() => {
if (ranOnce || !ref.current || Object.keys(groupsCoordinates).length === 0) if (!endpointY) return
return
const id = source.itemId ?? source.blockId const id = source.itemId ?? source.blockId
addSourceEndpoint({ addSourceEndpoint?.({
id, id,
ref, y: endpointY,
}) })
setRanOnce(true) }, [addSourceEndpoint, endpointY, source.blockId, source.itemId])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ref.current, groupsCoordinates])
useEventListener( useEventListener(
'pointerdown', 'pointerdown',
@ -55,6 +102,7 @@ export const SourceEndpoint = ({
if (!groupsCoordinates) return <></> if (!groupsCoordinates) return <></>
return ( return (
<Flex <Flex
ref={ref}
data-testid="endpoint" data-testid="endpoint"
boxSize="32px" boxSize="32px"
rounded="full" rounded="full"
@ -65,7 +113,6 @@ export const SourceEndpoint = ({
{...props} {...props}
> >
<Flex <Flex
ref={ref}
boxSize="20px" boxSize="20px"
justify="center" justify="center"
align="center" align="center"

View File

@ -1,26 +1,78 @@
import { Box, BoxProps } from '@chakra-ui/react' import { Box, BoxProps } from '@chakra-ui/react'
import React, {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useGraph } from '../../providers' import { useGraph } from '../../providers'
import React, { useEffect, useRef } from 'react' import { useEndpoints } from '../../providers/EndpointsProvider'
const endpointHeight = 20
export const TargetEndpoint = ({ export const TargetEndpoint = ({
groupId,
blockId, blockId,
isVisible,
...props ...props
}: BoxProps & { }: BoxProps & {
groupId: string
blockId: string blockId: string
isVisible?: boolean
}) => { }) => {
const { addTargetEndpoint } = useGraph() const { setTargetEnpointYOffset: addTargetEndpoint } = useEndpoints()
const { graphPosition } = useGraph()
const ref = useRef<HTMLDivElement | null>(null) const ref = useRef<HTMLDivElement | null>(null)
const [groupHeight, setGroupHeight] = useState<number>()
const [groupTransformProp, setGroupTransformProp] = useState<string>()
const endpointY = useMemo(
() =>
ref.current
? (ref.current?.getBoundingClientRect().y +
(endpointHeight * graphPosition.scale) / 2 -
graphPosition.y) /
graphPosition.scale
: undefined,
// We need to force recompute whenever the group height and position changes
// eslint-disable-next-line react-hooks/exhaustive-deps
[graphPosition.scale, graphPosition.y, groupHeight, groupTransformProp]
)
useLayoutEffect(() => {
const resizeObserver = new ResizeObserver((entries) => {
setGroupHeight(entries[0].contentRect.height)
})
const groupElement = document.getElementById(`group-${groupId}`)
if (!groupElement) return
resizeObserver.observe(groupElement)
return () => {
resizeObserver.disconnect()
}
}, [groupId])
useLayoutEffect(() => {
const mutationObserver = new MutationObserver((entries) => {
setGroupTransformProp((entries[0].target as HTMLElement).style.transform)
})
const groupElement = document.getElementById(`group-${groupId}`)
if (!groupElement) return
mutationObserver.observe(groupElement, {
attributes: true,
attributeFilter: ['style'],
})
return () => {
mutationObserver.disconnect()
}
}, [groupId])
useEffect(() => { useEffect(() => {
if (!ref.current) return if (!endpointY) return
addTargetEndpoint({ const id = blockId
id: blockId, addTargetEndpoint?.({
ref, id,
y: endpointY,
}) })
// eslint-disable-next-line react-hooks/exhaustive-deps }, [addTargetEndpoint, blockId, endpointY])
}, [ref])
return ( return (
<Box <Box
@ -29,7 +81,7 @@ export const TargetEndpoint = ({
rounded="full" rounded="full"
bgColor="blue.500" bgColor="blue.500"
cursor="pointer" cursor="pointer"
visibility={isVisible ? 'visible' : 'hidden'} visibility="hidden"
{...props} {...props}
/> />
) )

View File

@ -73,7 +73,6 @@ export const Graph = ({
editorContainerRef.current = document.getElementById( editorContainerRef.current = document.getElementById(
'editor-container' 'editor-container'
) as HTMLDivElement ) as HTMLDivElement
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
useEffect(() => { useEffect(() => {
@ -84,8 +83,7 @@ export const Graph = ({
y: top + debouncedGraphPosition.y, y: top + debouncedGraphPosition.y,
scale: debouncedGraphPosition.scale, scale: debouncedGraphPosition.scale,
}) })
// eslint-disable-next-line react-hooks/exhaustive-deps }, [debouncedGraphPosition, setGlobalGraphPosition])
}, [debouncedGraphPosition])
const handleMouseUp = (e: MouseEvent) => { const handleMouseUp = (e: MouseEvent) => {
if (!typebot) return if (!typebot) return
@ -304,5 +302,4 @@ const useAutoMoveBoard = (
return () => { return () => {
clearInterval(interval) clearInterval(interval)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [autoMoveDirection, setGraphPosition])
}, [autoMoveDirection])

View File

@ -1,6 +1,7 @@
import { AnswersCount } from '@/features/analytics' import { AnswersCount } from '@/features/analytics'
import { Edge, Group } from 'models' import { Edge, Group } from 'models'
import React, { memo } from 'react' import React, { memo } from 'react'
import { EndpointsProvider } from '../providers/EndpointsProvider'
import { Edges } from './Edges' import { Edges } from './Edges'
import { GroupNode } from './Nodes/GroupNode' import { GroupNode } from './Nodes/GroupNode'
@ -17,7 +18,7 @@ const GroupNodes = ({
onUnlockProPlanClick, onUnlockProPlanClick,
}: Props) => { }: Props) => {
return ( return (
<> <EndpointsProvider>
<Edges <Edges
edges={edges} edges={edges}
answersCounts={answersCounts} answersCounts={answersCounts}
@ -26,7 +27,7 @@ const GroupNodes = ({
{groups.map((group, idx) => ( {groups.map((group, idx) => (
<GroupNode group={group} groupIndex={idx} key={group.id} /> <GroupNode group={group} groupIndex={idx} key={group.id} />
))} ))}
</> </EndpointsProvider>
) )
} }

View File

@ -18,7 +18,7 @@ import {
TextBubbleBlock, TextBubbleBlock,
LogicBlockType, LogicBlockType,
} from 'models' } from 'models'
import { isBubbleBlock, isTextBubbleBlock } from 'utils' import { isBubbleBlock, isDefined, isTextBubbleBlock } from 'utils'
import { BlockNodeContent } from './BlockNodeContent/BlockNodeContent' import { BlockNodeContent } from './BlockNodeContent/BlockNodeContent'
import { BlockIcon, useTypebot } from '@/features/editor' import { BlockIcon, useTypebot } from '@/features/editor'
import { SettingsPopoverContent } from './SettingsPopoverContent' import { SettingsPopoverContent } from './SettingsPopoverContent'
@ -116,8 +116,8 @@ export const BlockNode = ({
const handleMouseEnter = () => { const handleMouseEnter = () => {
if (isReadOnly) return if (isReadOnly) return
if (mouseOverBlock?.id !== block.id) if (mouseOverBlock?.id !== block.id && blockRef.current)
setMouseOverBlock({ id: block.id, ref: blockRef }) setMouseOverBlock({ id: block.id, element: blockRef.current })
if (connectingIds) if (connectingIds)
setConnectingIds({ setConnectingIds({
...connectingIds, ...connectingIds,
@ -165,6 +165,10 @@ export const BlockNode = ({
useEventListener('pointerdown', (e) => e.stopPropagation(), blockRef.current) useEventListener('pointerdown', (e) => e.stopPropagation(), blockRef.current)
const hasIcomingEdge = typebot?.edges.some((edge) => {
return edge.to.blockId === block.id
})
return isEditing && isTextBubbleBlock(block) ? ( return isEditing && isTextBubbleBlock(block) ? (
<TextBubbleEditor <TextBubbleEditor
id={block.id} id={block.id}
@ -218,12 +222,15 @@ export const BlockNode = ({
data-testid={`${block.id}-icon`} data-testid={`${block.id}-icon`}
/> />
<BlockNodeContent block={block} indices={indices} /> <BlockNodeContent block={block} indices={indices} />
<TargetEndpoint {(hasIcomingEdge || isDefined(connectingIds)) && (
pos="absolute" <TargetEndpoint
left="-34px" pos="absolute"
top="16px" left="-34px"
blockId={block.id} top="16px"
/> blockId={block.id}
groupId={block.groupId}
/>
)}
{isConnectable && hasDefaultConnector(block) && ( {isConnectable && hasDefaultConnector(block) && (
<SourceEndpoint <SourceEndpoint
source={{ source={{

View File

@ -55,8 +55,7 @@ export const BlockNodesList = ({
useEffect(() => { useEffect(() => {
if (mouseOverGroup?.id !== groupId) setExpandedPlaceholderIndex(undefined) if (mouseOverGroup?.id !== groupId) setExpandedPlaceholderIndex(undefined)
// eslint-disable-next-line react-hooks/exhaustive-deps }, [groupId, mouseOverGroup?.id])
}, [mouseOverGroup?.id])
const handleMouseMoveGlobal = (event: MouseEvent) => { const handleMouseMoveGlobal = (event: MouseEvent) => {
if (!draggedBlock || draggedBlock.groupId !== groupId) return if (!draggedBlock || draggedBlock.groupId !== groupId) return
@ -114,14 +113,10 @@ export const BlockNodesList = ({
useEventListener('mousemove', handleMouseMoveGlobal) useEventListener('mousemove', handleMouseMoveGlobal)
useEventListener('mousemove', handleMouseMoveOnGroup, groupRef.current) useEventListener('mousemove', handleMouseMoveOnGroup, groupRef.current)
useEventListener( useEventListener('mouseup', handleMouseUpOnGroup, mouseOverGroup?.element, {
'mouseup', capture: true,
handleMouseUpOnGroup, })
mouseOverGroup?.ref.current,
{
capture: true,
}
)
return ( return (
<Stack <Stack
spacing={1} spacing={1}

View File

@ -7,6 +7,7 @@ import {
IconButton, IconButton,
HStack, HStack,
Stack, Stack,
useColorModeValue,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { ExpandIcon } from '@/components/icons' import { ExpandIcon } from '@/components/icons'
import { import {
@ -49,6 +50,7 @@ type Props = {
} }
export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => { export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
const arrowColor = useColorModeValue('white', 'gray.800')
const ref = useRef<HTMLDivElement | null>(null) const ref = useRef<HTMLDivElement | null>(null)
const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation() const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation()
@ -59,7 +61,7 @@ export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
return ( return (
<Portal> <Portal>
<PopoverContent onMouseDown={handleMouseDown} pos="relative"> <PopoverContent onMouseDown={handleMouseDown} pos="relative">
<PopoverArrow /> <PopoverArrow bgColor={arrowColor} />
<PopoverBody <PopoverBody
pt="3" pt="3"
pb="6" pb="6"

View File

@ -131,8 +131,8 @@ const NonMemoizedDraggableGroupNode = ({
const handleMouseEnter = () => { const handleMouseEnter = () => {
if (isReadOnly) return if (isReadOnly) return
if (mouseOverGroup?.id !== group.id && !isStartGroup) if (mouseOverGroup?.id !== group.id && !isStartGroup && groupRef.current)
setMouseOverGroup({ id: group.id, ref: groupRef }) setMouseOverGroup({ id: group.id, element: groupRef.current })
if (connectingIds) if (connectingIds)
setConnectingIds({ ...connectingIds, target: { groupId: group.id } }) setConnectingIds({ ...connectingIds, target: { groupId: group.id } })
} }
@ -185,6 +185,7 @@ const NonMemoizedDraggableGroupNode = ({
{(ref, isContextMenuOpened) => ( {(ref, isContextMenuOpened) => (
<Stack <Stack
ref={setMultipleRefs([ref, groupRef])} ref={setMultipleRefs([ref, groupRef])}
id={`group-${group.id}`}
data-testid="group" data-testid="group"
p="4" p="4"
rounded="xl" rounded="xl"
@ -239,24 +240,26 @@ const NonMemoizedDraggableGroupNode = ({
isStartGroup={isStartGroup} isStartGroup={isStartGroup}
/> />
)} )}
<SlideFade {!isReadOnly && (
in={isFocused} <SlideFade
style={{ in={isFocused}
position: 'absolute', style={{
top: '-50px', position: 'absolute',
right: 0, top: '-50px',
}} right: 0,
unmountOnExit
>
<GroupFocusToolbar
onPlayClick={startPreviewAtThisGroup}
onDuplicateClick={() => {
setIsFocused(false)
duplicateGroup(groupIndex)
}} }}
onDeleteClick={() => deleteGroup(groupIndex)} unmountOnExit
/> >
</SlideFade> <GroupFocusToolbar
onPlayClick={startPreviewAtThisGroup}
onDuplicateClick={() => {
setIsFocused(false)
duplicateGroup(groupIndex)
}}
onDeleteClick={() => deleteGroup(groupIndex)}
/>
</SlideFade>
)}
</Stack> </Stack>
)} )}
</ContextMenu> </ContextMenu>

View File

@ -67,19 +67,14 @@ export const ItemNodesList = ({
if (mouseOverBlock?.id !== block.id) { if (mouseOverBlock?.id !== block.id) {
setExpandedPlaceholderIndex(undefined) setExpandedPlaceholderIndex(undefined)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [block.id, mouseOverBlock?.id, showPlaceholders])
}, [mouseOverBlock?.id, showPlaceholders])
const handleMouseMoveOnBlock = (event: MouseEvent) => { const handleMouseMoveOnBlock = (event: MouseEvent) => {
if (!isDraggingOnCurrentBlock) return if (!isDraggingOnCurrentBlock) return
const index = computeNearestPlaceholderIndex(event.pageY, placeholderRefs) const index = computeNearestPlaceholderIndex(event.pageY, placeholderRefs)
setExpandedPlaceholderIndex(index) setExpandedPlaceholderIndex(index)
} }
useEventListener( useEventListener('mousemove', handleMouseMoveOnBlock, mouseOverBlock?.element)
'mousemove',
handleMouseMoveOnBlock,
mouseOverBlock?.ref.current
)
const handleMouseUpOnGroup = (e: MouseEvent) => { const handleMouseUpOnGroup = (e: MouseEvent) => {
if ( if (
@ -99,14 +94,9 @@ export const ItemNodesList = ({
itemIndex, itemIndex,
}) })
} }
useEventListener( useEventListener('mouseup', handleMouseUpOnGroup, mouseOverBlock?.element, {
'mouseup', capture: true,
handleMouseUpOnGroup, })
mouseOverBlock?.ref.current,
{
capture: true,
}
)
const handleBlockMouseDown = const handleBlockMouseDown =
(itemIndex: number) => (itemIndex: number) =>

View File

@ -0,0 +1,58 @@
import {
createContext,
ReactNode,
useCallback,
useContext,
useState,
} from 'react'
export type Endpoint = {
id: string
y: number
}
export const endpointsContext = createContext<{
sourceEndpointYOffsets: Map<string, Endpoint>
setSourceEndpointYOffset?: (endpoint: Endpoint) => void
targetEndpointYOffsets: Map<string, Endpoint>
setTargetEnpointYOffset?: (endpoint: Endpoint) => void
}>({
sourceEndpointYOffsets: new Map(),
targetEndpointYOffsets: new Map(),
})
export const EndpointsProvider = ({ children }: { children: ReactNode }) => {
const [sourceEndpointYOffsets, setSourceEndpoints] = useState<
Map<string, Endpoint>
>(new Map())
const [targetEndpointYOffsets, setTargetEndpoints] = useState<
Map<string, Endpoint>
>(new Map())
const setSourceEndpointYOffset = useCallback((endpoint: Endpoint) => {
setSourceEndpoints((endpoints) =>
new Map(endpoints).set(endpoint.id, endpoint)
)
}, [])
const setTargetEnpointYOffset = useCallback((endpoint: Endpoint) => {
setTargetEndpoints((endpoints) =>
new Map(endpoints).set(endpoint.id, endpoint)
)
}, [])
return (
<endpointsContext.Provider
value={{
sourceEndpointYOffsets,
targetEndpointYOffsets,
setSourceEndpointYOffset,
setTargetEnpointYOffset,
}}
>
{children}
</endpointsContext.Provider>
)
}
export const useEndpoints = () => useContext(endpointsContext)

View File

@ -5,15 +5,16 @@ import {
Dispatch, Dispatch,
ReactNode, ReactNode,
SetStateAction, SetStateAction,
useCallback,
useContext, useContext,
useRef, useRef,
useState, useState,
} from 'react' } from 'react'
import { Coordinates } from './GraphProvider' import { Coordinates } from './GraphProvider'
type NodeInfo = { type NodeElement = {
id: string id: string
ref: React.MutableRefObject<HTMLDivElement | null> element: HTMLDivElement
} }
const graphDndContext = createContext<{ const graphDndContext = createContext<{
@ -23,10 +24,10 @@ const graphDndContext = createContext<{
setDraggedBlock: Dispatch<SetStateAction<DraggableBlock | undefined>> setDraggedBlock: Dispatch<SetStateAction<DraggableBlock | undefined>>
draggedItem?: Item draggedItem?: Item
setDraggedItem: Dispatch<SetStateAction<Item | undefined>> setDraggedItem: Dispatch<SetStateAction<Item | undefined>>
mouseOverGroup?: NodeInfo mouseOverGroup?: NodeElement
setMouseOverGroup: Dispatch<SetStateAction<NodeInfo | undefined>> setMouseOverGroup: (node: NodeElement | undefined) => void
mouseOverBlock?: NodeInfo mouseOverBlock?: NodeElement
setMouseOverBlock: Dispatch<SetStateAction<NodeInfo | undefined>> setMouseOverBlock: (node: NodeElement | undefined) => void
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore //@ts-ignore
}>({}) }>({})
@ -39,8 +40,24 @@ export const GraphDndProvider = ({ children }: { children: ReactNode }) => {
DraggableBlockType | undefined DraggableBlockType | undefined
>() >()
const [draggedItem, setDraggedItem] = useState<Item | undefined>() const [draggedItem, setDraggedItem] = useState<Item | undefined>()
const [mouseOverGroup, setMouseOverGroup] = useState<NodeInfo>() const [mouseOverGroup, _setMouseOverGroup] = useState<NodeElement>()
const [mouseOverBlock, setMouseOverBlock] = useState<NodeInfo>() const [mouseOverBlock, _setMouseOverBlock] = useState<NodeElement>()
const setMouseOverGroup = useCallback(
(node: NodeElement | undefined) => {
if (node && !draggedBlock && !draggedBlockType) return
_setMouseOverGroup(node)
},
[draggedBlock, draggedBlockType]
)
const setMouseOverBlock = useCallback(
(node: NodeElement | undefined) => {
if (node && !draggedItem) return
_setMouseOverBlock(node)
},
[draggedItem]
)
return ( return (
<graphDndContext.Provider <graphDndContext.Provider

View File

@ -2,7 +2,6 @@ import { Group, Edge, IdMap, Source, Block, Target } from 'models'
import { import {
createContext, createContext,
Dispatch, Dispatch,
MutableRefObject,
ReactNode, ReactNode,
SetStateAction, SetStateAction,
useContext, useContext,
@ -53,13 +52,6 @@ export type ConnectingIds = {
target?: Target target?: Target
} }
type BlockId = string
type ButtonId = string
export type Endpoint = {
id: BlockId | ButtonId
ref: MutableRefObject<HTMLDivElement | null>
}
export type GroupsCoordinates = IdMap<Coordinates> export type GroupsCoordinates = IdMap<Coordinates>
type PreviewingBlock = { type PreviewingBlock = {
@ -76,10 +68,6 @@ const graphContext = createContext<{
setPreviewingBlock: Dispatch<SetStateAction<PreviewingBlock | undefined>> setPreviewingBlock: Dispatch<SetStateAction<PreviewingBlock | undefined>>
previewingEdge?: Edge previewingEdge?: Edge
setPreviewingEdge: Dispatch<SetStateAction<Edge | undefined>> setPreviewingEdge: Dispatch<SetStateAction<Edge | undefined>>
sourceEndpoints: IdMap<Endpoint>
addSourceEndpoint: (endpoint: Endpoint) => void
targetEndpoints: IdMap<Endpoint>
addTargetEndpoint: (endpoint: Endpoint) => void
openedBlockId?: string openedBlockId?: string
setOpenedBlockId: Dispatch<SetStateAction<string | undefined>> setOpenedBlockId: Dispatch<SetStateAction<string | undefined>>
openedItemId?: string openedItemId?: string
@ -107,26 +95,10 @@ export const GraphProvider = ({
const [connectingIds, setConnectingIds] = useState<ConnectingIds | null>(null) const [connectingIds, setConnectingIds] = useState<ConnectingIds | null>(null)
const [previewingEdge, setPreviewingEdge] = useState<Edge>() const [previewingEdge, setPreviewingEdge] = useState<Edge>()
const [previewingBlock, setPreviewingBlock] = useState<PreviewingBlock>() const [previewingBlock, setPreviewingBlock] = useState<PreviewingBlock>()
const [sourceEndpoints, setSourceEndpoints] = useState<IdMap<Endpoint>>({})
const [targetEndpoints, setTargetEndpoints] = useState<IdMap<Endpoint>>({})
const [openedBlockId, setOpenedBlockId] = useState<string>() const [openedBlockId, setOpenedBlockId] = useState<string>()
const [openedItemId, setOpenedItemId] = useState<string>() const [openedItemId, setOpenedItemId] = useState<string>()
const [focusedGroupId, setFocusedGroupId] = useState<string>() const [focusedGroupId, setFocusedGroupId] = useState<string>()
const addSourceEndpoint = (endpoint: Endpoint) => {
setSourceEndpoints((endpoints) => ({
...endpoints,
[endpoint.id]: endpoint,
}))
}
const addTargetEndpoint = (endpoint: Endpoint) => {
setTargetEndpoints((endpoints) => ({
...endpoints,
[endpoint.id]: endpoint,
}))
}
return ( return (
<graphContext.Provider <graphContext.Provider
value={{ value={{
@ -136,10 +108,6 @@ export const GraphProvider = ({
setConnectingIds, setConnectingIds,
previewingEdge, previewingEdge,
setPreviewingEdge, setPreviewingEdge,
sourceEndpoints,
targetEndpoints,
addSourceEndpoint,
addTargetEndpoint,
openedBlockId, openedBlockId,
setOpenedBlockId, setOpenedBlockId,
openedItemId, openedItemId,

View File

@ -31,14 +31,13 @@ export const GroupsCoordinatesProvider = ({
useEffect(() => { useEffect(() => {
setGroupsCoordinates( setGroupsCoordinates(
groups.reduce( groups.reduce(
(coords, block) => ({ (coords, group) => ({
...coords, ...coords,
[block.id]: block.graphCoordinates, [group.id]: group.graphCoordinates,
}), }),
{} {}
) )
) )
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [groups]) }, [groups])
const updateGroupCoordinates = useCallback( const updateGroupCoordinates = useCallback(

View File

@ -29,8 +29,6 @@ import {
defaultWebhookOptions, defaultWebhookOptions,
DraggableBlock, DraggableBlock,
DraggableBlockType, DraggableBlockType,
Edge,
IdMap,
InputBlockType, InputBlockType,
IntegrationBlockType, IntegrationBlockType,
Item, Item,
@ -43,7 +41,6 @@ import {
stubLength, stubLength,
blockWidth, blockWidth,
blockAnchorsOffset, blockAnchorsOffset,
Endpoint,
Coordinates, Coordinates,
} from './providers' } from './providers'
import { roundCorners } from 'svg-round-corners' import { roundCorners } from 'svg-round-corners'
@ -330,32 +327,6 @@ export const computeEdgePathToMouse = ({
).path ).path
} }
export const getEndpointTopOffset = ({
endpoints,
graphOffsetY,
endpointId,
graphScale,
}: {
endpoints: IdMap<Endpoint>
graphOffsetY: number
endpointId?: string
graphScale: number
}): number | undefined => {
if (!endpointId) return
const endpointRef = endpoints[endpointId]?.ref
if (!endpointRef?.current) return
const endpointHeight = 20 * graphScale
return (
(endpointRef.current.getBoundingClientRect().y +
endpointHeight / 2 -
graphOffsetY) /
graphScale
)
}
export const getSourceEndpointId = (edge?: Edge) =>
edge?.from.itemId ?? edge?.from.blockId
export const parseNewBlock = ( export const parseNewBlock = (
type: DraggableBlockType, type: DraggableBlockType,
groupId: string groupId: string