⚡ (editor) Improve edges responsiveness
Also fixed a ton of useEffects should make everything a bit more reactive. Closes #307
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user