⚡ (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
|
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) =>
|
||||||
|
@ -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,
|
||||||
[
|
[
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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>
|
||||||
|
@ -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 undo = useCallback(() => {
|
||||||
}
|
const { past, present, future } = history
|
||||||
|
if (past.length === 0 || !present) return
|
||||||
|
|
||||||
const previous = past[past.length - 1]
|
const previous = past[past.length - 1]
|
||||||
const newPast = past.slice(0, past.length - 1)
|
const newPast = past.slice(0, past.length - 1)
|
||||||
|
|
||||||
return {
|
const newPresent = { ...previous, updatedAt: present.updatedAt }
|
||||||
past: newPast,
|
|
||||||
present: { ...previous, updatedAt: present.updatedAt },
|
|
||||||
future: [present, ...future],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case ActionType.Redo: {
|
setHistory({
|
||||||
if (future.length === 0) {
|
past: newPast,
|
||||||
return state
|
present: newPresent,
|
||||||
}
|
future: [present, ...future],
|
||||||
|
})
|
||||||
|
presentRef.current = newPresent
|
||||||
|
}, [history])
|
||||||
|
|
||||||
|
const redo = useCallback(() => {
|
||||||
|
const { past, present, future } = history
|
||||||
|
if (future.length === 0) return
|
||||||
const next = future[0]
|
const next = future[0]
|
||||||
const newFuture = future.slice(1)
|
const newFuture = future.slice(1)
|
||||||
|
|
||||||
return {
|
setHistory({
|
||||||
past: [...past, present],
|
past: present ? [...past, present] : past,
|
||||||
present: next,
|
present: next,
|
||||||
future: newFuture,
|
future: newFuture,
|
||||||
}
|
})
|
||||||
}
|
presentRef.current = next
|
||||||
|
}, [history])
|
||||||
|
|
||||||
case ActionType.Set: {
|
const set = useCallback(
|
||||||
const { newPresent } = action
|
(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: [],
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
presentRef.current = newPresent
|
||||||
|
},
|
||||||
const canUndo = state.past.length !== 0
|
[history]
|
||||||
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(() => {
|
const flush = useCallback(() => {
|
||||||
dispatch({ type: ActionType.Flush })
|
setHistory({
|
||||||
|
present: presentRef.current ?? undefined,
|
||||||
|
past: [],
|
||||||
|
future: [],
|
||||||
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return [
|
return [history.present, { set, undo, redo, flush, canUndo, canRedo }]
|
||||||
state as State<T>,
|
|
||||||
{ set, undo, redo, flush, canUndo, canRedo, presentRef },
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useUndo
|
|
||||||
|
@ -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,
|
||||||
|
publishedTypebot,
|
||||||
|
webhooks,
|
||||||
|
isReadOnly,
|
||||||
|
isLoading: isFetchingTypebot,
|
||||||
|
mutate,
|
||||||
|
} = useTypebotQuery({
|
||||||
typebotId,
|
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 })
|
||||||
|
@ -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
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -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])
|
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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} />
|
||||||
|
{(hasIcomingEdge || isDefined(connectingIds)) && (
|
||||||
<TargetEndpoint
|
<TargetEndpoint
|
||||||
pos="absolute"
|
pos="absolute"
|
||||||
left="-34px"
|
left="-34px"
|
||||||
top="16px"
|
top="16px"
|
||||||
blockId={block.id}
|
blockId={block.id}
|
||||||
|
groupId={block.groupId}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{isConnectable && hasDefaultConnector(block) && (
|
{isConnectable && hasDefaultConnector(block) && (
|
||||||
<SourceEndpoint
|
<SourceEndpoint
|
||||||
source={{
|
source={{
|
||||||
|
@ -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',
|
|
||||||
handleMouseUpOnGroup,
|
|
||||||
mouseOverGroup?.ref.current,
|
|
||||||
{
|
|
||||||
capture: true,
|
capture: true,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
spacing={1}
|
spacing={1}
|
||||||
|
@ -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"
|
||||||
|
@ -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,6 +240,7 @@ const NonMemoizedDraggableGroupNode = ({
|
|||||||
isStartGroup={isStartGroup}
|
isStartGroup={isStartGroup}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{!isReadOnly && (
|
||||||
<SlideFade
|
<SlideFade
|
||||||
in={isFocused}
|
in={isFocused}
|
||||||
style={{
|
style={{
|
||||||
@ -257,6 +259,7 @@ const NonMemoizedDraggableGroupNode = ({
|
|||||||
onDeleteClick={() => deleteGroup(groupIndex)}
|
onDeleteClick={() => deleteGroup(groupIndex)}
|
||||||
/>
|
/>
|
||||||
</SlideFade>
|
</SlideFade>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
@ -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',
|
|
||||||
handleMouseUpOnGroup,
|
|
||||||
mouseOverBlock?.ref.current,
|
|
||||||
{
|
|
||||||
capture: true,
|
capture: true,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
const handleBlockMouseDown =
|
const handleBlockMouseDown =
|
||||||
(itemIndex: number) =>
|
(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,
|
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
|
||||||
|
@ -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,
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user