(editor) Improve edges responsiveness

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

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

View File

@@ -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>

View File

@@ -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

View File

@@ -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 })

View File

@@ -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