refactor(editor): ♻️ Undo / Redo buttons + structure refacto
Yet another huge refacto... While implementing undo and redo features I understood that I updated the stored typebot too many times (i.e. on each key input) so I had to rethink it entirely. I also moved around some files.
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
import { Block, Edge, Table, Target, Typebot } from 'models'
|
||||
import { AnchorsPositionProps } from 'components/board/graph/Edges/Edge'
|
||||
import { Edge, Table, Target } from 'models'
|
||||
import { AnchorsPositionProps } from 'components/shared/Graph/Edges/Edge'
|
||||
import {
|
||||
stubLength,
|
||||
blockWidth,
|
||||
@ -7,6 +7,7 @@ import {
|
||||
ConnectingIds,
|
||||
Endpoint,
|
||||
Coordinates,
|
||||
BlocksCoordinates,
|
||||
} from 'contexts/GraphContext'
|
||||
import { roundCorners } from 'svg-round-corners'
|
||||
import { headerHeight } from 'components/shared/TypebotHeader'
|
||||
@ -122,34 +123,32 @@ const computeFiveSegments = (
|
||||
}
|
||||
|
||||
type GetAnchorsPositionParams = {
|
||||
sourceBlock: Block
|
||||
targetBlock: Block
|
||||
sourceBlockCoordinates: Coordinates
|
||||
targetBlockCoordinates: Coordinates
|
||||
sourceTop: number
|
||||
targetTop: number
|
||||
targetTop?: number
|
||||
}
|
||||
export const getAnchorsPosition = ({
|
||||
sourceBlock,
|
||||
targetBlock,
|
||||
sourceBlockCoordinates,
|
||||
targetBlockCoordinates,
|
||||
sourceTop,
|
||||
targetTop,
|
||||
}: GetAnchorsPositionParams): AnchorsPositionProps => {
|
||||
const targetOffsetY = targetTop > 0 ? targetTop : undefined
|
||||
|
||||
const sourcePosition = computeSourceCoordinates(
|
||||
sourceBlock.graphCoordinates,
|
||||
sourceBlockCoordinates,
|
||||
sourceTop
|
||||
)
|
||||
let sourceType: 'right' | 'left' = 'right'
|
||||
if (sourceBlock.graphCoordinates.x > targetBlock.graphCoordinates.x) {
|
||||
sourcePosition.x = sourceBlock.graphCoordinates.x
|
||||
if (sourceBlockCoordinates.x > targetBlockCoordinates.x) {
|
||||
sourcePosition.x = sourceBlockCoordinates.x
|
||||
sourceType = 'left'
|
||||
}
|
||||
|
||||
const { targetPosition, totalSegments } = computeBlockTargetPosition(
|
||||
sourceBlock.graphCoordinates,
|
||||
targetBlock.graphCoordinates,
|
||||
sourceBlockCoordinates,
|
||||
targetBlockCoordinates,
|
||||
sourcePosition.y,
|
||||
targetOffsetY
|
||||
targetTop
|
||||
)
|
||||
return { sourcePosition, targetPosition, sourceType, totalSegments }
|
||||
}
|
||||
@ -157,11 +156,11 @@ export const getAnchorsPosition = ({
|
||||
const computeBlockTargetPosition = (
|
||||
sourceBlockPosition: Coordinates,
|
||||
targetBlockPosition: Coordinates,
|
||||
offsetY: number,
|
||||
sourceOffsetY: number,
|
||||
targetOffsetY?: number
|
||||
): { targetPosition: Coordinates; totalSegments: number } => {
|
||||
const isTargetBlockBelow =
|
||||
targetBlockPosition.y > offsetY &&
|
||||
targetBlockPosition.y > sourceOffsetY &&
|
||||
targetBlockPosition.x < sourceBlockPosition.x + blockWidth + stubLength &&
|
||||
targetBlockPosition.x > sourceBlockPosition.x - blockWidth - stubLength
|
||||
const isTargetBlockToTheRight = targetBlockPosition.x < sourceBlockPosition.x
|
||||
@ -230,19 +229,24 @@ export const computeEdgePath = ({
|
||||
).path
|
||||
}
|
||||
|
||||
export const computeConnectingEdgePath = (
|
||||
connectingIds: Omit<ConnectingIds, 'target'> & { target: Target },
|
||||
sourceBlock: Block,
|
||||
sourceTop: number,
|
||||
targetTop: number,
|
||||
typebot: Typebot
|
||||
) => {
|
||||
if (!sourceBlock) return ``
|
||||
const targetBlock = typebot.blocks.byId[connectingIds.target.blockId]
|
||||
|
||||
export const computeConnectingEdgePath = ({
|
||||
connectingIds,
|
||||
sourceTop,
|
||||
targetTop,
|
||||
blocksCoordinates,
|
||||
}: {
|
||||
connectingIds: Omit<ConnectingIds, 'target'> & { target: Target }
|
||||
sourceTop: number
|
||||
targetTop?: number
|
||||
blocksCoordinates: BlocksCoordinates
|
||||
}) => {
|
||||
const sourceBlockCoordinates =
|
||||
blocksCoordinates.byId[connectingIds.source.blockId]
|
||||
const targetBlockCoordinates =
|
||||
blocksCoordinates.byId[connectingIds.target.blockId]
|
||||
const anchorsPosition = getAnchorsPosition({
|
||||
sourceBlock,
|
||||
targetBlock,
|
||||
sourceBlockCoordinates,
|
||||
targetBlockCoordinates,
|
||||
sourceTop,
|
||||
targetTop,
|
||||
})
|
||||
@ -282,10 +286,10 @@ export const getEndpointTopOffset = (
|
||||
graphPosition: Coordinates,
|
||||
endpoints: Table<Endpoint>,
|
||||
endpointId?: string
|
||||
): number => {
|
||||
if (!endpointId) return 0
|
||||
): number | undefined => {
|
||||
if (!endpointId) return
|
||||
const endpointRef = endpoints.byId[endpointId]?.ref
|
||||
if (!endpointRef) return 0
|
||||
if (!endpointRef) return
|
||||
return (
|
||||
8 +
|
||||
(endpointRef.current?.getBoundingClientRect().top ?? 0) -
|
||||
@ -295,4 +299,4 @@ export const getEndpointTopOffset = (
|
||||
}
|
||||
|
||||
export const getSourceEndpointId = (edge?: Edge) =>
|
||||
edge?.from.nodeId ?? edge?.from.stepId + `${edge?.from.conditionType ?? ''}`
|
||||
edge?.from.buttonId ?? edge?.from.stepId + `${edge?.from.conditionType ?? ''}`
|
||||
|
@ -2,7 +2,7 @@ import { PublicTypebot, Typebot } from 'models'
|
||||
import shortId from 'short-uuid'
|
||||
import { HStack, Text } from '@chakra-ui/react'
|
||||
import { CalendarIcon } from 'assets/icons'
|
||||
import { StepIcon } from 'components/board/StepsSideBar/StepIcon'
|
||||
import { StepIcon } from 'components/editor/StepsSideBar/StepIcon'
|
||||
import { isInputStep, sendRequest } from 'utils'
|
||||
|
||||
export const parseTypebotToPublicTypebot = (
|
||||
|
@ -31,7 +31,7 @@ import {
|
||||
defaultWebhookOptions,
|
||||
StepWithOptionsType,
|
||||
} from 'models'
|
||||
import shortId from 'short-uuid'
|
||||
import shortId, { generate } from 'short-uuid'
|
||||
import { Typebot } from 'models'
|
||||
import useSWR from 'swr'
|
||||
import { fetcher, toKebabCase } from './utils'
|
||||
@ -112,22 +112,6 @@ export const patchTypebot = async (id: string, typebot: Partial<Typebot>) =>
|
||||
body: typebot,
|
||||
})
|
||||
|
||||
export const parseNewBlock = ({
|
||||
totalBlocks,
|
||||
initialCoordinates,
|
||||
}: {
|
||||
totalBlocks: number
|
||||
initialCoordinates: { x: number; y: number }
|
||||
}): Block => {
|
||||
const id = `b${shortId.generate()}`
|
||||
return {
|
||||
id,
|
||||
title: `Block #${totalBlocks + 1}`,
|
||||
graphCoordinates: initialCoordinates,
|
||||
stepIds: [],
|
||||
}
|
||||
}
|
||||
|
||||
export const parseNewStep = (
|
||||
type: DraggableStepType,
|
||||
blockId: string
|
||||
@ -170,7 +154,7 @@ const parseDefaultStepOptions = (type: StepWithOptionsType): StepOptions => {
|
||||
case InputStepType.URL:
|
||||
return defaultUrlInputOptions
|
||||
case InputStepType.CHOICE:
|
||||
return defaultChoiceInputOptions
|
||||
return { ...defaultChoiceInputOptions, itemIds: [generate()] }
|
||||
case LogicStepType.SET_VARIABLE:
|
||||
return defaultSetVariablesOptions
|
||||
case LogicStepType.CONDITION:
|
||||
@ -182,7 +166,7 @@ const parseDefaultStepOptions = (type: StepWithOptionsType): StepOptions => {
|
||||
case IntegrationStepType.GOOGLE_ANALYTICS:
|
||||
return defaultGoogleAnalyticsOptions
|
||||
case IntegrationStepType.WEBHOOK:
|
||||
return defaultWebhookOptions
|
||||
return { ...defaultWebhookOptions, webhookId: generate() }
|
||||
}
|
||||
}
|
||||
|
||||
|
2
apps/builder/services/utils/index.ts
Normal file
2
apps/builder/services/utils/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './utils'
|
||||
export * from './useUndo'
|
127
apps/builder/services/utils/useUndo.ts
Normal file
127
apps/builder/services/utils/useUndo.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import { isDefined } from '@udecode/plate-core'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
// import { diff } from 'deep-object-diff'
|
||||
import { useReducer, useCallback, useRef } from 'react'
|
||||
import { isNotDefined } from 'utils'
|
||||
|
||||
enum ActionType {
|
||||
Undo = 'UNDO',
|
||||
Redo = 'REDO',
|
||||
Set = 'SET',
|
||||
}
|
||||
|
||||
export interface Actions<T> {
|
||||
set: (newPresent: T) => void
|
||||
undo: () => void
|
||||
redo: () => void
|
||||
canUndo: boolean
|
||||
canRedo: boolean
|
||||
presentRef: React.MutableRefObject<T>
|
||||
}
|
||||
|
||||
interface Action<T> {
|
||||
type: ActionType
|
||||
newPresent?: T
|
||||
}
|
||||
|
||||
export interface State<T> {
|
||||
past: T[]
|
||||
present: T
|
||||
future: T[]
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
past: [],
|
||||
present: null,
|
||||
future: [],
|
||||
}
|
||||
|
||||
const reducer = <T>(state: State<T>, action: Action<T>) => {
|
||||
const { past, present, future } = state
|
||||
|
||||
switch (action.type) {
|
||||
case ActionType.Undo: {
|
||||
if (past.length === 0) {
|
||||
return state
|
||||
}
|
||||
|
||||
const previous = past[past.length - 1]
|
||||
const newPast = past.slice(0, past.length - 1)
|
||||
|
||||
return {
|
||||
past: newPast,
|
||||
present: previous,
|
||||
future: [present, ...future],
|
||||
}
|
||||
}
|
||||
|
||||
case ActionType.Redo: {
|
||||
if (future.length === 0) {
|
||||
return state
|
||||
}
|
||||
const next = future[0]
|
||||
const newFuture = future.slice(1)
|
||||
|
||||
return {
|
||||
past: [...past, present],
|
||||
present: next,
|
||||
future: newFuture,
|
||||
}
|
||||
}
|
||||
|
||||
case ActionType.Set: {
|
||||
const { newPresent } = action
|
||||
if (
|
||||
isNotDefined(newPresent) ||
|
||||
(present &&
|
||||
deepEqual(
|
||||
JSON.parse(JSON.stringify(newPresent)),
|
||||
JSON.parse(JSON.stringify(present))
|
||||
))
|
||||
) {
|
||||
return state
|
||||
}
|
||||
// Uncomment to debug history ⬇️
|
||||
// console.log(
|
||||
// diff(
|
||||
// JSON.parse(JSON.stringify(newPresent)),
|
||||
// JSON.parse(JSON.stringify(present))
|
||||
// )
|
||||
// )
|
||||
return {
|
||||
past: [...past, present].filter(isDefined),
|
||||
present: newPresent,
|
||||
future: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const useUndo = <T>(initialPresent: T): [State<T>, Actions<T>] => {
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
...initialState,
|
||||
present: initialPresent,
|
||||
}) as [State<T>, React.Dispatch<Action<T>>]
|
||||
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) => {
|
||||
presentRef.current = newPresent
|
||||
dispatch({ type: ActionType.Set, newPresent })
|
||||
}, [])
|
||||
|
||||
return [state, { set, undo, redo, canUndo, canRedo, presentRef }]
|
||||
}
|
||||
|
||||
export default useUndo
|
Reference in New Issue
Block a user