2
0

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:
Baptiste Arnaud
2022-02-02 08:05:02 +01:00
parent fc1d654772
commit 8a350eee6c
153 changed files with 1512 additions and 1352 deletions

View File

@ -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 ?? ''}`

View File

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

View File

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

View File

@ -0,0 +1,2 @@
export * from './utils'
export * from './useUndo'

View 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