⚡ (editor) Improve edges responsiveness
Also fixed a ton of useEffects should make everything a bit more reactive. Closes #307
This commit is contained in:
@ -20,9 +20,11 @@ const authenticateByToken = async (
|
||||
apiToken: string
|
||||
): Promise<User | undefined> => {
|
||||
if (typeof window !== 'undefined') return
|
||||
return (await prisma.user.findFirst({
|
||||
const user = (await prisma.user.findFirst({
|
||||
where: { apiTokens: { some: { token: apiToken } } },
|
||||
})) as User
|
||||
setUser({ id: user.id, email: user.email ?? undefined })
|
||||
return user
|
||||
}
|
||||
|
||||
const extractBearerToken = (req: NextApiRequest) =>
|
||||
|
@ -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=$30.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(
|
||||
planChangeWorkspaceId,
|
||||
[
|
||||
|
@ -162,6 +162,7 @@ export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
|
||||
</Stack>
|
||||
<SwitchWithLabel
|
||||
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}
|
||||
onCheckChange={handleIsCustomBodyChange}
|
||||
/>
|
||||
|
@ -44,7 +44,7 @@ export const EditorPage = () => {
|
||||
{!isReadOnly && <BlocksSideBar />}
|
||||
<GraphProvider isReadOnly={isReadOnly}>
|
||||
<GroupsCoordinatesProvider groups={typebot.groups}>
|
||||
<Graph flex="1" typebot={typebot} />
|
||||
<Graph flex="1" typebot={typebot} key={typebot.id} />
|
||||
<BoardMenuButton pos="absolute" right="40px" top="20px" />
|
||||
<RightPanel />
|
||||
</GroupsCoordinatesProvider>
|
||||
|
@ -1,152 +1,108 @@
|
||||
import { isDefined } from '@udecode/plate-core'
|
||||
import { dequal } from 'dequal'
|
||||
// import { diff } from 'deep-object-diff'
|
||||
import { useReducer, useCallback, useRef } from 'react'
|
||||
import { isNotDefined } from 'utils'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
|
||||
enum ActionType {
|
||||
Undo = 'UNDO',
|
||||
Redo = 'REDO',
|
||||
Set = 'SET',
|
||||
Flush = 'FLUSH',
|
||||
}
|
||||
|
||||
export interface Actions<T extends { updatedAt: Date } | undefined> {
|
||||
set: (
|
||||
newPresent: T | ((current: T) => T),
|
||||
options?: { updateDate: boolean }
|
||||
) => void
|
||||
export interface Actions<T extends { updatedAt: Date }> {
|
||||
set: (newPresent: T | ((current: T) => T) | undefined) => void
|
||||
undo: () => void
|
||||
redo: () => void
|
||||
flush: () => void
|
||||
canUndo: boolean
|
||||
canRedo: boolean
|
||||
presentRef: React.MutableRefObject<T>
|
||||
}
|
||||
|
||||
interface Action<T extends { updatedAt: Date } | undefined> {
|
||||
type: ActionType
|
||||
newPresent?: T
|
||||
updateDate?: boolean
|
||||
}
|
||||
|
||||
export interface State<T extends { updatedAt: Date } | undefined> {
|
||||
export interface History<T extends { updatedAt: Date }> {
|
||||
past: T[]
|
||||
present: T
|
||||
present: T | undefined
|
||||
future: T[]
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
past: [],
|
||||
present: null,
|
||||
present: undefined,
|
||||
future: [],
|
||||
}
|
||||
|
||||
const reducer = <T extends { updatedAt: Date } | undefined>(
|
||||
state: State<T>,
|
||||
action: Action<T>
|
||||
) => {
|
||||
const { past, present, future } = state
|
||||
export const useUndo = <T extends { updatedAt: Date }>(
|
||||
initialPresent?: T
|
||||
): [T | undefined, Actions<T>] => {
|
||||
const [history, setHistory] = useState<History<T>>(initialState)
|
||||
const presentRef = useRef<T | null>(initialPresent ?? null)
|
||||
|
||||
switch (action.type) {
|
||||
case ActionType.Undo: {
|
||||
if (past.length === 0 || !present) {
|
||||
return state
|
||||
}
|
||||
const canUndo = history.past.length !== 0
|
||||
const canRedo = history.future.length !== 0
|
||||
|
||||
const previous = past[past.length - 1]
|
||||
const newPast = past.slice(0, past.length - 1)
|
||||
const undo = useCallback(() => {
|
||||
const { past, present, future } = history
|
||||
if (past.length === 0 || !present) return
|
||||
|
||||
return {
|
||||
past: newPast,
|
||||
present: { ...previous, updatedAt: present.updatedAt },
|
||||
future: [present, ...future],
|
||||
}
|
||||
}
|
||||
const previous = past[past.length - 1]
|
||||
const newPast = past.slice(0, past.length - 1)
|
||||
|
||||
case ActionType.Redo: {
|
||||
if (future.length === 0) {
|
||||
return state
|
||||
}
|
||||
const next = future[0]
|
||||
const newFuture = future.slice(1)
|
||||
const newPresent = { ...previous, updatedAt: present.updatedAt }
|
||||
|
||||
return {
|
||||
past: [...past, present],
|
||||
present: next,
|
||||
future: newFuture,
|
||||
}
|
||||
}
|
||||
setHistory({
|
||||
past: newPast,
|
||||
present: newPresent,
|
||||
future: [present, ...future],
|
||||
})
|
||||
presentRef.current = newPresent
|
||||
}, [history])
|
||||
|
||||
case ActionType.Set: {
|
||||
const { newPresent } = action
|
||||
const redo = useCallback(() => {
|
||||
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 (
|
||||
isNotDefined(newPresent) ||
|
||||
(present &&
|
||||
dequal(
|
||||
JSON.parse(JSON.stringify(newPresent)),
|
||||
JSON.parse(JSON.stringify(present))
|
||||
))
|
||||
newPresent &&
|
||||
present &&
|
||||
dequal(
|
||||
JSON.parse(JSON.stringify(newPresent)),
|
||||
JSON.parse(JSON.stringify(present))
|
||||
)
|
||||
) {
|
||||
return state
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
if (newPresent === undefined) {
|
||||
presentRef.current = null
|
||||
setHistory(initialState)
|
||||
return
|
||||
}
|
||||
setHistory({
|
||||
past: [...past, present].filter(isDefined),
|
||||
present: newPresent,
|
||||
future: [],
|
||||
}
|
||||
}
|
||||
})
|
||||
presentRef.current = newPresent
|
||||
},
|
||||
[history]
|
||||
)
|
||||
|
||||
case ActionType.Flush:
|
||||
return { ...initialState, present }
|
||||
}
|
||||
}
|
||||
|
||||
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(() => {
|
||||
setHistory({
|
||||
present: presentRef.current ?? undefined,
|
||||
past: [],
|
||||
future: [],
|
||||
})
|
||||
}, [])
|
||||
|
||||
const flush = useCallback(() => {
|
||||
dispatch({ type: ActionType.Flush })
|
||||
}, [])
|
||||
|
||||
return [
|
||||
state as State<T>,
|
||||
{ set, undo, redo, flush, canUndo, canRedo, presentRef },
|
||||
]
|
||||
return [history.present, { set, undo, redo, flush, canUndo, canRedo }]
|
||||
}
|
||||
|
||||
export default useUndo
|
||||
|
@ -26,7 +26,7 @@ import { variablesAction, VariablesActions } from './actions/variables'
|
||||
import { dequal } from 'dequal'
|
||||
import { useToast } from '@/hooks/useToast'
|
||||
import { useTypebotQuery } from '@/hooks/useTypebotQuery'
|
||||
import useUndo from '../../hooks/useUndo'
|
||||
import { useUndo } from '../../hooks/useUndo'
|
||||
import { useLinkedTypebots } from '@/hooks/useLinkedTypebots'
|
||||
import { updateTypebotQuery } from '../../queries/updateTypebotQuery'
|
||||
import { preventUserFromRefreshing } from '@/utils/helpers'
|
||||
@ -46,7 +46,6 @@ import {
|
||||
import { useAutoSave } from '@/hooks/useAutoSave'
|
||||
import { createWebhookQuery } from '@/features/blocks/integrations/webhook/queries/createWebhookQuery'
|
||||
import { duplicateWebhookQuery } from '@/features/blocks/integrations/webhook/queries/duplicateWebhookQuery'
|
||||
import { useSession } from 'next-auth/react'
|
||||
|
||||
const autoSaveTimeout = 10000
|
||||
|
||||
@ -104,27 +103,24 @@ export const TypebotProvider = ({
|
||||
children: ReactNode
|
||||
typebotId?: string
|
||||
}) => {
|
||||
const { status } = useSession()
|
||||
const { push } = useRouter()
|
||||
const { showToast } = useToast()
|
||||
|
||||
const { typebot, publishedTypebot, webhooks, isReadOnly, isLoading, mutate } =
|
||||
useTypebotQuery({
|
||||
typebotId,
|
||||
})
|
||||
const {
|
||||
typebot,
|
||||
publishedTypebot,
|
||||
webhooks,
|
||||
isReadOnly,
|
||||
isLoading: isFetchingTypebot,
|
||||
mutate,
|
||||
} = useTypebotQuery({
|
||||
typebotId,
|
||||
})
|
||||
|
||||
const [
|
||||
{ present: localTypebot },
|
||||
{
|
||||
redo,
|
||||
undo,
|
||||
flush,
|
||||
canRedo,
|
||||
canUndo,
|
||||
set: setLocalTypebot,
|
||||
presentRef: currentTypebotRef,
|
||||
},
|
||||
] = useUndo<Typebot | undefined>(undefined)
|
||||
localTypebot,
|
||||
{ redo, undo, flush, canRedo, canUndo, set: setLocalTypebot },
|
||||
] = useUndo<Typebot>(undefined)
|
||||
|
||||
const linkedTypebotIds =
|
||||
localTypebot?.groups
|
||||
@ -151,23 +147,34 @@ export const TypebotProvider = ({
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!typebot || !currentTypebotRef.current) return
|
||||
if (typebotId !== currentTypebotRef.current.id) {
|
||||
setLocalTypebot({ ...typebot }, { updateDate: false })
|
||||
flush()
|
||||
} else if (
|
||||
if (!typebot && isDefined(localTypebot)) setLocalTypebot(undefined)
|
||||
if (isFetchingTypebot) return
|
||||
if (!typebot) {
|
||||
showToast({ status: 'info', description: "Couldn't find typebot" })
|
||||
push('/typebots')
|
||||
return
|
||||
}
|
||||
if (
|
||||
typebot.id !== localTypebot?.id ||
|
||||
new Date(typebot.updatedAt).getTime() >
|
||||
new Date(currentTypebotRef.current.updatedAt).getTime()
|
||||
new Date(localTypebot.updatedAt).getTime()
|
||||
) {
|
||||
setLocalTypebot({ ...typebot })
|
||||
flush()
|
||||
}
|
||||
}, [
|
||||
flush,
|
||||
isFetchingTypebot,
|
||||
localTypebot,
|
||||
push,
|
||||
setLocalTypebot,
|
||||
showToast,
|
||||
typebot,
|
||||
])
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [typebot])
|
||||
|
||||
const saveTypebot = async () => {
|
||||
if (!currentTypebotRef.current || !typebot) return
|
||||
const typebotToSave = { ...currentTypebotRef.current }
|
||||
const saveTypebot = useCallback(async () => {
|
||||
if (!localTypebot || !typebot) return
|
||||
const typebotToSave = { ...localTypebot }
|
||||
if (dequal(omit(typebot, 'updatedAt'), omit(typebotToSave, 'updatedAt')))
|
||||
return
|
||||
setIsSavingLoading(true)
|
||||
@ -187,7 +194,15 @@ export const TypebotProvider = ({
|
||||
webhooks: webhooks ?? [],
|
||||
})
|
||||
window.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||
}
|
||||
}, [
|
||||
localTypebot,
|
||||
mutate,
|
||||
publishedTypebot,
|
||||
setLocalTypebot,
|
||||
showToast,
|
||||
typebot,
|
||||
webhooks,
|
||||
])
|
||||
|
||||
const savePublishedTypebot = async (newPublishedTypebot: PublicTypebot) => {
|
||||
if (!localTypebot) return
|
||||
@ -201,7 +216,7 @@ export const TypebotProvider = ({
|
||||
if (error)
|
||||
return showToast({ title: error.name, description: error.message })
|
||||
mutate({
|
||||
typebot: currentTypebotRef.current as Typebot,
|
||||
typebot: localTypebot,
|
||||
publishedTypebot: newPublishedTypebot,
|
||||
webhooks: webhooks ?? [],
|
||||
})
|
||||
@ -213,7 +228,7 @@ export const TypebotProvider = ({
|
||||
item: localTypebot,
|
||||
debounceTimeout: autoSaveTimeout,
|
||||
},
|
||||
[typebot, publishedTypebot, webhooks]
|
||||
[saveTypebot, localTypebot]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@ -221,8 +236,7 @@ export const TypebotProvider = ({
|
||||
return () => {
|
||||
Router.events.off('routeChangeStart', saveTypebot)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [typebot, publishedTypebot, webhooks])
|
||||
}, [saveTypebot])
|
||||
|
||||
const [isSavingLoading, setIsSavingLoading] = useState(false)
|
||||
const [isPublishing, setIsPublishing] = useState(false)
|
||||
@ -237,26 +251,14 @@ export const TypebotProvider = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (!localTypebot || !typebot) return
|
||||
currentTypebotRef.current = localTypebot
|
||||
if (!checkIfTypebotsAreEqual(localTypebot, typebot)) {
|
||||
window.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||
window.addEventListener('beforeunload', preventUserFromRefreshing)
|
||||
} else {
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [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])
|
||||
}, [localTypebot, typebot])
|
||||
|
||||
const updateLocalTypebot = (updates: UpdateTypebotPayload) =>
|
||||
localTypebot && setLocalTypebot({ ...localTypebot, ...updates })
|
||||
|
@ -34,8 +34,10 @@ export const edgesAction = (setTypebot: SetTypebot): EdgesActions => ({
|
||||
)
|
||||
const itemIndex = edge.from.itemId
|
||||
? (
|
||||
typebot.groups[groupIndex].blocks[blockIndex] as BlockWithItems
|
||||
).items.findIndex(byId(edge.from.itemId))
|
||||
typebot.groups[groupIndex].blocks[blockIndex] as
|
||||
| BlockWithItems
|
||||
| undefined
|
||||
)?.items.findIndex(byId(edge.from.itemId))
|
||||
: null
|
||||
|
||||
isDefined(itemIndex) && itemIndex !== -1
|
||||
|
@ -9,20 +9,15 @@ import {
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { colors } from '@/lib/theme'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import {
|
||||
computeConnectingEdgePath,
|
||||
computeEdgePathToMouse,
|
||||
getEndpointTopOffset,
|
||||
} from '../../utils'
|
||||
import { computeConnectingEdgePath, computeEdgePathToMouse } from '../../utils'
|
||||
import { useEndpoints } from '../../providers/EndpointsProvider'
|
||||
|
||||
export const DrawingEdge = () => {
|
||||
const { graphPosition, setConnectingIds, connectingIds } = useGraph()
|
||||
const {
|
||||
graphPosition,
|
||||
setConnectingIds,
|
||||
connectingIds,
|
||||
sourceEndpoints,
|
||||
targetEndpoints,
|
||||
} = useGraph()
|
||||
sourceEndpointYOffsets: sourceEndpoints,
|
||||
targetEndpointYOffsets: targetEndpoints,
|
||||
} = useEndpoints()
|
||||
const { groupsCoordinates } = useGroupsCoordinates()
|
||||
const { createEdge } = useTypebot()
|
||||
const [mousePosition, setMousePosition] = useState<Coordinates | null>(null)
|
||||
@ -34,24 +29,15 @@ export const DrawingEdge = () => {
|
||||
|
||||
const sourceTop = useMemo(() => {
|
||||
if (!connectingIds) return 0
|
||||
return getEndpointTopOffset({
|
||||
endpoints: sourceEndpoints,
|
||||
graphOffsetY: graphPosition.y,
|
||||
endpointId: connectingIds.source.itemId ?? connectingIds.source.blockId,
|
||||
graphScale: graphPosition.scale,
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const endpointId =
|
||||
connectingIds.source.itemId ?? connectingIds.source.blockId
|
||||
return sourceEndpoints.get(endpointId)?.y
|
||||
}, [connectingIds, sourceEndpoints])
|
||||
|
||||
const targetTop = useMemo(() => {
|
||||
if (!connectingIds) return 0
|
||||
return getEndpointTopOffset({
|
||||
endpoints: targetEndpoints,
|
||||
graphOffsetY: graphPosition.y,
|
||||
endpointId: connectingIds.target?.blockId,
|
||||
graphScale: graphPosition.scale,
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const endpointId = connectingIds.target?.blockId
|
||||
return endpointId ? targetEndpoints.get(endpointId)?.y : undefined
|
||||
}, [connectingIds, targetEndpoints])
|
||||
|
||||
const path = useMemo(() => {
|
||||
|
@ -6,18 +6,15 @@ import {
|
||||
useColorModeValue,
|
||||
theme,
|
||||
} from '@chakra-ui/react'
|
||||
import { useGraph, useGroupsCoordinates } from '../../providers'
|
||||
import { useGroupsCoordinates } from '../../providers'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { useWorkspace } from '@/features/workspace'
|
||||
import React, { useMemo } from 'react'
|
||||
import { byId, isDefined } from 'utils'
|
||||
import { isProPlan } from '@/features/billing'
|
||||
import { AnswersCount } from '@/features/analytics'
|
||||
import {
|
||||
getEndpointTopOffset,
|
||||
computeSourceCoordinates,
|
||||
computeDropOffPath,
|
||||
} from '../../utils'
|
||||
import { computeSourceCoordinates, computeDropOffPath } from '../../utils'
|
||||
import { useEndpoints } from '../../providers/EndpointsProvider'
|
||||
|
||||
type Props = {
|
||||
groupId: string
|
||||
@ -36,7 +33,7 @@ export const DropOffEdge = ({
|
||||
)
|
||||
const { workspace } = useWorkspace()
|
||||
const { groupsCoordinates } = useGroupsCoordinates()
|
||||
const { sourceEndpoints, graphPosition } = useGraph()
|
||||
const { sourceEndpointYOffsets: sourceEndpoints } = useEndpoints()
|
||||
const { publishedTypebot } = useTypebot()
|
||||
|
||||
const isWorkspaceProPlan = isProPlan(workspace)
|
||||
@ -68,17 +65,11 @@ export const DropOffEdge = ({
|
||||
}, [answersCounts, groupId, totalAnswers, publishedTypebot])
|
||||
|
||||
const group = publishedTypebot?.groups.find(byId(groupId))
|
||||
const sourceTop = useMemo(
|
||||
() =>
|
||||
getEndpointTopOffset({
|
||||
endpoints: sourceEndpoints,
|
||||
graphOffsetY: graphPosition.y,
|
||||
endpointId: group?.blocks[group.blocks.length - 1].id,
|
||||
graphScale: graphPosition.scale,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[group?.blocks, sourceEndpoints, groupsCoordinates]
|
||||
)
|
||||
|
||||
const sourceTop = useMemo(() => {
|
||||
const endpointId = group?.blocks[group.blocks.length - 1].id
|
||||
return endpointId ? sourceEndpoints.get(endpointId)?.y : undefined
|
||||
}, [group?.blocks, sourceEndpoints])
|
||||
|
||||
const labelCoordinates = useMemo(() => {
|
||||
if (!groupsCoordinates[groupId]) return
|
||||
|
@ -1,16 +1,12 @@
|
||||
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 { Portal, useColorMode, useDisclosure } from '@chakra-ui/react'
|
||||
import { useTypebot } from '@/features/editor'
|
||||
import { EdgeMenu } from './EdgeMenu'
|
||||
import { colors } from '@/lib/theme'
|
||||
import {
|
||||
getEndpointTopOffset,
|
||||
getSourceEndpointId,
|
||||
getAnchorsPosition,
|
||||
computeEdgePath,
|
||||
} from '../../utils'
|
||||
import { getAnchorsPosition, computeEdgePath } from '../../utils'
|
||||
import { useEndpoints } from '../../providers/EndpointsProvider'
|
||||
|
||||
export type AnchorsPositionProps = {
|
||||
sourcePosition: Coordinates
|
||||
@ -22,22 +18,17 @@ export type AnchorsPositionProps = {
|
||||
type Props = {
|
||||
edge: EdgeProps
|
||||
}
|
||||
|
||||
export const Edge = ({ edge }: Props) => {
|
||||
const isDark = useColorMode().colorMode === 'dark'
|
||||
const { deleteEdge } = useTypebot()
|
||||
const {
|
||||
previewingEdge,
|
||||
sourceEndpoints,
|
||||
targetEndpoints,
|
||||
graphPosition,
|
||||
isReadOnly,
|
||||
setPreviewingEdge,
|
||||
} = useGraph()
|
||||
const { previewingEdge, graphPosition, isReadOnly, setPreviewingEdge } =
|
||||
useGraph()
|
||||
const { sourceEndpointYOffsets, targetEndpointYOffsets } = useEndpoints()
|
||||
const { groupsCoordinates } = useGroupsCoordinates()
|
||||
const [isMouseOver, setIsMouseOver] = useState(false)
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const [edgeMenuPosition, setEdgeMenuPosition] = useState({ x: 0, y: 0 })
|
||||
const [refreshEdge, setRefreshEdge] = useState(false)
|
||||
|
||||
const isPreviewing = isMouseOver || previewingEdge?.id === edge.id
|
||||
|
||||
@ -46,47 +37,20 @@ export const Edge = ({ edge }: Props) => {
|
||||
const targetGroupCoordinates =
|
||||
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({
|
||||
endpoints: sourceEndpoints,
|
||||
graphOffsetY: graphPosition.y,
|
||||
endpointId: getSourceEndpointId(edge),
|
||||
graphScale: graphPosition.scale,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[sourceGroupCoordinates?.y, edge, sourceEndpoints, refreshEdge]
|
||||
edge?.to.blockId
|
||||
? targetEndpointYOffsets.get(edge?.to.blockId)?.y
|
||||
: undefined,
|
||||
[edge?.to.blockId, targetEndpointYOffsets]
|
||||
)
|
||||
|
||||
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(() => {
|
||||
if (!sourceGroupCoordinates || !targetGroupCoordinates || !sourceTop)
|
||||
return ``
|
||||
@ -98,13 +62,12 @@ export const Edge = ({ edge }: Props) => {
|
||||
graphScale: graphPosition.scale,
|
||||
})
|
||||
return computeEdgePath(anchorsPosition)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
sourceGroupCoordinates?.x,
|
||||
sourceGroupCoordinates?.y,
|
||||
targetGroupCoordinates?.x,
|
||||
targetGroupCoordinates?.y,
|
||||
sourceGroupCoordinates,
|
||||
targetGroupCoordinates,
|
||||
sourceTop,
|
||||
targetTop,
|
||||
graphPosition.scale,
|
||||
])
|
||||
|
||||
const handleMouseEnter = () => setIsMouseOver(true)
|
||||
|
@ -6,7 +6,16 @@ import {
|
||||
} from '@chakra-ui/react'
|
||||
import { useGraph, useGroupsCoordinates } from '../../providers'
|
||||
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 = ({
|
||||
source,
|
||||
@ -17,23 +26,61 @@ export const SourceEndpoint = ({
|
||||
const color = useColorModeValue('blue.200', 'blue.100')
|
||||
const connectedColor = useColorModeValue('blue.300', 'blue.200')
|
||||
const bg = useColorModeValue('gray.100', 'gray.700')
|
||||
const [ranOnce, setRanOnce] = useState(false)
|
||||
const { setConnectingIds, addSourceEndpoint, previewingEdge } = useGraph()
|
||||
|
||||
const { setConnectingIds, previewingEdge, graphPosition } = useGraph()
|
||||
const { setSourceEndpointYOffset: addSourceEndpoint } = useEndpoints()
|
||||
const { groupsCoordinates } = useGroupsCoordinates()
|
||||
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(() => {
|
||||
if (ranOnce || !ref.current || Object.keys(groupsCoordinates).length === 0)
|
||||
return
|
||||
if (!endpointY) return
|
||||
const id = source.itemId ?? source.blockId
|
||||
addSourceEndpoint({
|
||||
addSourceEndpoint?.({
|
||||
id,
|
||||
ref,
|
||||
y: endpointY,
|
||||
})
|
||||
setRanOnce(true)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ref.current, groupsCoordinates])
|
||||
}, [addSourceEndpoint, endpointY, source.blockId, source.itemId])
|
||||
|
||||
useEventListener(
|
||||
'pointerdown',
|
||||
@ -55,6 +102,7 @@ export const SourceEndpoint = ({
|
||||
if (!groupsCoordinates) return <></>
|
||||
return (
|
||||
<Flex
|
||||
ref={ref}
|
||||
data-testid="endpoint"
|
||||
boxSize="32px"
|
||||
rounded="full"
|
||||
@ -65,7 +113,6 @@ export const SourceEndpoint = ({
|
||||
{...props}
|
||||
>
|
||||
<Flex
|
||||
ref={ref}
|
||||
boxSize="20px"
|
||||
justify="center"
|
||||
align="center"
|
||||
|
@ -1,26 +1,78 @@
|
||||
import { Box, BoxProps } from '@chakra-ui/react'
|
||||
import React, {
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useGraph } from '../../providers'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { useEndpoints } from '../../providers/EndpointsProvider'
|
||||
|
||||
const endpointHeight = 20
|
||||
|
||||
export const TargetEndpoint = ({
|
||||
groupId,
|
||||
blockId,
|
||||
isVisible,
|
||||
...props
|
||||
}: BoxProps & {
|
||||
groupId: string
|
||||
blockId: string
|
||||
isVisible?: boolean
|
||||
}) => {
|
||||
const { addTargetEndpoint } = useGraph()
|
||||
const { setTargetEnpointYOffset: addTargetEndpoint } = useEndpoints()
|
||||
const { graphPosition } = useGraph()
|
||||
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(() => {
|
||||
if (!ref.current) return
|
||||
addTargetEndpoint({
|
||||
id: blockId,
|
||||
ref,
|
||||
if (!endpointY) return
|
||||
const id = blockId
|
||||
addTargetEndpoint?.({
|
||||
id,
|
||||
y: endpointY,
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ref])
|
||||
}, [addTargetEndpoint, blockId, endpointY])
|
||||
|
||||
return (
|
||||
<Box
|
||||
@ -29,7 +81,7 @@ export const TargetEndpoint = ({
|
||||
rounded="full"
|
||||
bgColor="blue.500"
|
||||
cursor="pointer"
|
||||
visibility={isVisible ? 'visible' : 'hidden'}
|
||||
visibility="hidden"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
@ -73,7 +73,6 @@ export const Graph = ({
|
||||
editorContainerRef.current = document.getElementById(
|
||||
'editor-container'
|
||||
) as HTMLDivElement
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@ -84,8 +83,7 @@ export const Graph = ({
|
||||
y: top + debouncedGraphPosition.y,
|
||||
scale: debouncedGraphPosition.scale,
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedGraphPosition])
|
||||
}, [debouncedGraphPosition, setGlobalGraphPosition])
|
||||
|
||||
const handleMouseUp = (e: MouseEvent) => {
|
||||
if (!typebot) return
|
||||
@ -304,5 +302,4 @@ const useAutoMoveBoard = (
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [autoMoveDirection])
|
||||
}, [autoMoveDirection, setGraphPosition])
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { AnswersCount } from '@/features/analytics'
|
||||
import { Edge, Group } from 'models'
|
||||
import React, { memo } from 'react'
|
||||
import { EndpointsProvider } from '../providers/EndpointsProvider'
|
||||
import { Edges } from './Edges'
|
||||
import { GroupNode } from './Nodes/GroupNode'
|
||||
|
||||
@ -17,7 +18,7 @@ const GroupNodes = ({
|
||||
onUnlockProPlanClick,
|
||||
}: Props) => {
|
||||
return (
|
||||
<>
|
||||
<EndpointsProvider>
|
||||
<Edges
|
||||
edges={edges}
|
||||
answersCounts={answersCounts}
|
||||
@ -26,7 +27,7 @@ const GroupNodes = ({
|
||||
{groups.map((group, idx) => (
|
||||
<GroupNode group={group} groupIndex={idx} key={group.id} />
|
||||
))}
|
||||
</>
|
||||
</EndpointsProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ import {
|
||||
TextBubbleBlock,
|
||||
LogicBlockType,
|
||||
} from 'models'
|
||||
import { isBubbleBlock, isTextBubbleBlock } from 'utils'
|
||||
import { isBubbleBlock, isDefined, isTextBubbleBlock } from 'utils'
|
||||
import { BlockNodeContent } from './BlockNodeContent/BlockNodeContent'
|
||||
import { BlockIcon, useTypebot } from '@/features/editor'
|
||||
import { SettingsPopoverContent } from './SettingsPopoverContent'
|
||||
@ -116,8 +116,8 @@ export const BlockNode = ({
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (isReadOnly) return
|
||||
if (mouseOverBlock?.id !== block.id)
|
||||
setMouseOverBlock({ id: block.id, ref: blockRef })
|
||||
if (mouseOverBlock?.id !== block.id && blockRef.current)
|
||||
setMouseOverBlock({ id: block.id, element: blockRef.current })
|
||||
if (connectingIds)
|
||||
setConnectingIds({
|
||||
...connectingIds,
|
||||
@ -165,6 +165,10 @@ export const BlockNode = ({
|
||||
|
||||
useEventListener('pointerdown', (e) => e.stopPropagation(), blockRef.current)
|
||||
|
||||
const hasIcomingEdge = typebot?.edges.some((edge) => {
|
||||
return edge.to.blockId === block.id
|
||||
})
|
||||
|
||||
return isEditing && isTextBubbleBlock(block) ? (
|
||||
<TextBubbleEditor
|
||||
id={block.id}
|
||||
@ -218,12 +222,15 @@ export const BlockNode = ({
|
||||
data-testid={`${block.id}-icon`}
|
||||
/>
|
||||
<BlockNodeContent block={block} indices={indices} />
|
||||
<TargetEndpoint
|
||||
pos="absolute"
|
||||
left="-34px"
|
||||
top="16px"
|
||||
blockId={block.id}
|
||||
/>
|
||||
{(hasIcomingEdge || isDefined(connectingIds)) && (
|
||||
<TargetEndpoint
|
||||
pos="absolute"
|
||||
left="-34px"
|
||||
top="16px"
|
||||
blockId={block.id}
|
||||
groupId={block.groupId}
|
||||
/>
|
||||
)}
|
||||
{isConnectable && hasDefaultConnector(block) && (
|
||||
<SourceEndpoint
|
||||
source={{
|
||||
|
@ -55,8 +55,7 @@ export const BlockNodesList = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (mouseOverGroup?.id !== groupId) setExpandedPlaceholderIndex(undefined)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mouseOverGroup?.id])
|
||||
}, [groupId, mouseOverGroup?.id])
|
||||
|
||||
const handleMouseMoveGlobal = (event: MouseEvent) => {
|
||||
if (!draggedBlock || draggedBlock.groupId !== groupId) return
|
||||
@ -114,14 +113,10 @@ export const BlockNodesList = ({
|
||||
|
||||
useEventListener('mousemove', handleMouseMoveGlobal)
|
||||
useEventListener('mousemove', handleMouseMoveOnGroup, groupRef.current)
|
||||
useEventListener(
|
||||
'mouseup',
|
||||
handleMouseUpOnGroup,
|
||||
mouseOverGroup?.ref.current,
|
||||
{
|
||||
capture: true,
|
||||
}
|
||||
)
|
||||
useEventListener('mouseup', handleMouseUpOnGroup, mouseOverGroup?.element, {
|
||||
capture: true,
|
||||
})
|
||||
|
||||
return (
|
||||
<Stack
|
||||
spacing={1}
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
IconButton,
|
||||
HStack,
|
||||
Stack,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react'
|
||||
import { ExpandIcon } from '@/components/icons'
|
||||
import {
|
||||
@ -49,6 +50,7 @@ type Props = {
|
||||
}
|
||||
|
||||
export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
|
||||
const arrowColor = useColorModeValue('white', 'gray.800')
|
||||
const ref = useRef<HTMLDivElement | null>(null)
|
||||
const handleMouseDown = (e: React.MouseEvent) => e.stopPropagation()
|
||||
|
||||
@ -59,7 +61,7 @@ export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
|
||||
return (
|
||||
<Portal>
|
||||
<PopoverContent onMouseDown={handleMouseDown} pos="relative">
|
||||
<PopoverArrow />
|
||||
<PopoverArrow bgColor={arrowColor} />
|
||||
<PopoverBody
|
||||
pt="3"
|
||||
pb="6"
|
||||
|
@ -131,8 +131,8 @@ const NonMemoizedDraggableGroupNode = ({
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (isReadOnly) return
|
||||
if (mouseOverGroup?.id !== group.id && !isStartGroup)
|
||||
setMouseOverGroup({ id: group.id, ref: groupRef })
|
||||
if (mouseOverGroup?.id !== group.id && !isStartGroup && groupRef.current)
|
||||
setMouseOverGroup({ id: group.id, element: groupRef.current })
|
||||
if (connectingIds)
|
||||
setConnectingIds({ ...connectingIds, target: { groupId: group.id } })
|
||||
}
|
||||
@ -185,6 +185,7 @@ const NonMemoizedDraggableGroupNode = ({
|
||||
{(ref, isContextMenuOpened) => (
|
||||
<Stack
|
||||
ref={setMultipleRefs([ref, groupRef])}
|
||||
id={`group-${group.id}`}
|
||||
data-testid="group"
|
||||
p="4"
|
||||
rounded="xl"
|
||||
@ -239,24 +240,26 @@ const NonMemoizedDraggableGroupNode = ({
|
||||
isStartGroup={isStartGroup}
|
||||
/>
|
||||
)}
|
||||
<SlideFade
|
||||
in={isFocused}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-50px',
|
||||
right: 0,
|
||||
}}
|
||||
unmountOnExit
|
||||
>
|
||||
<GroupFocusToolbar
|
||||
onPlayClick={startPreviewAtThisGroup}
|
||||
onDuplicateClick={() => {
|
||||
setIsFocused(false)
|
||||
duplicateGroup(groupIndex)
|
||||
{!isReadOnly && (
|
||||
<SlideFade
|
||||
in={isFocused}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-50px',
|
||||
right: 0,
|
||||
}}
|
||||
onDeleteClick={() => deleteGroup(groupIndex)}
|
||||
/>
|
||||
</SlideFade>
|
||||
unmountOnExit
|
||||
>
|
||||
<GroupFocusToolbar
|
||||
onPlayClick={startPreviewAtThisGroup}
|
||||
onDuplicateClick={() => {
|
||||
setIsFocused(false)
|
||||
duplicateGroup(groupIndex)
|
||||
}}
|
||||
onDeleteClick={() => deleteGroup(groupIndex)}
|
||||
/>
|
||||
</SlideFade>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</ContextMenu>
|
||||
|
@ -67,19 +67,14 @@ export const ItemNodesList = ({
|
||||
if (mouseOverBlock?.id !== block.id) {
|
||||
setExpandedPlaceholderIndex(undefined)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [mouseOverBlock?.id, showPlaceholders])
|
||||
}, [block.id, mouseOverBlock?.id, showPlaceholders])
|
||||
|
||||
const handleMouseMoveOnBlock = (event: MouseEvent) => {
|
||||
if (!isDraggingOnCurrentBlock) return
|
||||
const index = computeNearestPlaceholderIndex(event.pageY, placeholderRefs)
|
||||
setExpandedPlaceholderIndex(index)
|
||||
}
|
||||
useEventListener(
|
||||
'mousemove',
|
||||
handleMouseMoveOnBlock,
|
||||
mouseOverBlock?.ref.current
|
||||
)
|
||||
useEventListener('mousemove', handleMouseMoveOnBlock, mouseOverBlock?.element)
|
||||
|
||||
const handleMouseUpOnGroup = (e: MouseEvent) => {
|
||||
if (
|
||||
@ -99,14 +94,9 @@ export const ItemNodesList = ({
|
||||
itemIndex,
|
||||
})
|
||||
}
|
||||
useEventListener(
|
||||
'mouseup',
|
||||
handleMouseUpOnGroup,
|
||||
mouseOverBlock?.ref.current,
|
||||
{
|
||||
capture: true,
|
||||
}
|
||||
)
|
||||
useEventListener('mouseup', handleMouseUpOnGroup, mouseOverBlock?.element, {
|
||||
capture: true,
|
||||
})
|
||||
|
||||
const handleBlockMouseDown =
|
||||
(itemIndex: number) =>
|
||||
|
@ -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)
|
@ -5,15 +5,16 @@ import {
|
||||
Dispatch,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useContext,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { Coordinates } from './GraphProvider'
|
||||
|
||||
type NodeInfo = {
|
||||
type NodeElement = {
|
||||
id: string
|
||||
ref: React.MutableRefObject<HTMLDivElement | null>
|
||||
element: HTMLDivElement
|
||||
}
|
||||
|
||||
const graphDndContext = createContext<{
|
||||
@ -23,10 +24,10 @@ const graphDndContext = createContext<{
|
||||
setDraggedBlock: Dispatch<SetStateAction<DraggableBlock | undefined>>
|
||||
draggedItem?: Item
|
||||
setDraggedItem: Dispatch<SetStateAction<Item | undefined>>
|
||||
mouseOverGroup?: NodeInfo
|
||||
setMouseOverGroup: Dispatch<SetStateAction<NodeInfo | undefined>>
|
||||
mouseOverBlock?: NodeInfo
|
||||
setMouseOverBlock: Dispatch<SetStateAction<NodeInfo | undefined>>
|
||||
mouseOverGroup?: NodeElement
|
||||
setMouseOverGroup: (node: NodeElement | undefined) => void
|
||||
mouseOverBlock?: NodeElement
|
||||
setMouseOverBlock: (node: NodeElement | undefined) => void
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
}>({})
|
||||
@ -39,8 +40,24 @@ export const GraphDndProvider = ({ children }: { children: ReactNode }) => {
|
||||
DraggableBlockType | undefined
|
||||
>()
|
||||
const [draggedItem, setDraggedItem] = useState<Item | undefined>()
|
||||
const [mouseOverGroup, setMouseOverGroup] = useState<NodeInfo>()
|
||||
const [mouseOverBlock, setMouseOverBlock] = useState<NodeInfo>()
|
||||
const [mouseOverGroup, _setMouseOverGroup] = useState<NodeElement>()
|
||||
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 (
|
||||
<graphDndContext.Provider
|
||||
|
@ -2,7 +2,6 @@ import { Group, Edge, IdMap, Source, Block, Target } from 'models'
|
||||
import {
|
||||
createContext,
|
||||
Dispatch,
|
||||
MutableRefObject,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
useContext,
|
||||
@ -53,13 +52,6 @@ export type ConnectingIds = {
|
||||
target?: Target
|
||||
}
|
||||
|
||||
type BlockId = string
|
||||
type ButtonId = string
|
||||
export type Endpoint = {
|
||||
id: BlockId | ButtonId
|
||||
ref: MutableRefObject<HTMLDivElement | null>
|
||||
}
|
||||
|
||||
export type GroupsCoordinates = IdMap<Coordinates>
|
||||
|
||||
type PreviewingBlock = {
|
||||
@ -76,10 +68,6 @@ const graphContext = createContext<{
|
||||
setPreviewingBlock: Dispatch<SetStateAction<PreviewingBlock | undefined>>
|
||||
previewingEdge?: Edge
|
||||
setPreviewingEdge: Dispatch<SetStateAction<Edge | undefined>>
|
||||
sourceEndpoints: IdMap<Endpoint>
|
||||
addSourceEndpoint: (endpoint: Endpoint) => void
|
||||
targetEndpoints: IdMap<Endpoint>
|
||||
addTargetEndpoint: (endpoint: Endpoint) => void
|
||||
openedBlockId?: string
|
||||
setOpenedBlockId: Dispatch<SetStateAction<string | undefined>>
|
||||
openedItemId?: string
|
||||
@ -107,26 +95,10 @@ export const GraphProvider = ({
|
||||
const [connectingIds, setConnectingIds] = useState<ConnectingIds | null>(null)
|
||||
const [previewingEdge, setPreviewingEdge] = useState<Edge>()
|
||||
const [previewingBlock, setPreviewingBlock] = useState<PreviewingBlock>()
|
||||
const [sourceEndpoints, setSourceEndpoints] = useState<IdMap<Endpoint>>({})
|
||||
const [targetEndpoints, setTargetEndpoints] = useState<IdMap<Endpoint>>({})
|
||||
const [openedBlockId, setOpenedBlockId] = useState<string>()
|
||||
const [openedItemId, setOpenedItemId] = 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 (
|
||||
<graphContext.Provider
|
||||
value={{
|
||||
@ -136,10 +108,6 @@ export const GraphProvider = ({
|
||||
setConnectingIds,
|
||||
previewingEdge,
|
||||
setPreviewingEdge,
|
||||
sourceEndpoints,
|
||||
targetEndpoints,
|
||||
addSourceEndpoint,
|
||||
addTargetEndpoint,
|
||||
openedBlockId,
|
||||
setOpenedBlockId,
|
||||
openedItemId,
|
||||
|
@ -31,14 +31,13 @@ export const GroupsCoordinatesProvider = ({
|
||||
useEffect(() => {
|
||||
setGroupsCoordinates(
|
||||
groups.reduce(
|
||||
(coords, block) => ({
|
||||
(coords, group) => ({
|
||||
...coords,
|
||||
[block.id]: block.graphCoordinates,
|
||||
[group.id]: group.graphCoordinates,
|
||||
}),
|
||||
{}
|
||||
)
|
||||
)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [groups])
|
||||
|
||||
const updateGroupCoordinates = useCallback(
|
||||
|
@ -29,8 +29,6 @@ import {
|
||||
defaultWebhookOptions,
|
||||
DraggableBlock,
|
||||
DraggableBlockType,
|
||||
Edge,
|
||||
IdMap,
|
||||
InputBlockType,
|
||||
IntegrationBlockType,
|
||||
Item,
|
||||
@ -43,7 +41,6 @@ import {
|
||||
stubLength,
|
||||
blockWidth,
|
||||
blockAnchorsOffset,
|
||||
Endpoint,
|
||||
Coordinates,
|
||||
} from './providers'
|
||||
import { roundCorners } from 'svg-round-corners'
|
||||
@ -330,32 +327,6 @@ export const computeEdgePathToMouse = ({
|
||||
).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 = (
|
||||
type: DraggableBlockType,
|
||||
groupId: string
|
||||
|
Reference in New Issue
Block a user