2
0

chore(editor): ♻️ Revert tables to arrays

Yet another refacto. I improved many many mechanisms on this one including dnd. It is now end 2 end tested 🎉
This commit is contained in:
Baptiste Arnaud
2022-02-04 19:00:08 +01:00
parent 8a350eee6c
commit 524ef0812c
123 changed files with 2998 additions and 3112 deletions

View File

@ -1,4 +1,4 @@
import { Block, Source, Step, Table, Target, Typebot } from 'models'
import { Block, Edge, IdMap, Source, Step, Target } from 'models'
import {
createContext,
Dispatch,
@ -9,7 +9,6 @@ import {
useEffect,
useState,
} from 'react'
import { useImmer } from 'use-immer'
export const stubLength = 20
export const blockWidth = 300
@ -56,20 +55,20 @@ export type Endpoint = {
ref: MutableRefObject<HTMLDivElement | null>
}
export type BlocksCoordinates = { byId: { [key: string]: Coordinates } }
export type BlocksCoordinates = IdMap<Coordinates>
const graphContext = createContext<{
blocksCoordinates?: BlocksCoordinates
blocksCoordinates: BlocksCoordinates
updateBlockCoordinates: (blockId: string, newCoord: Coordinates) => void
graphPosition: Position
setGraphPosition: Dispatch<SetStateAction<Position>>
connectingIds: ConnectingIds | null
setConnectingIds: Dispatch<SetStateAction<ConnectingIds | null>>
previewingEdgeId?: string
setPreviewingEdgeId: Dispatch<SetStateAction<string | undefined>>
sourceEndpoints: Table<Endpoint>
previewingEdge?: Edge
setPreviewingEdge: Dispatch<SetStateAction<Edge | undefined>>
sourceEndpoints: IdMap<Endpoint>
addSourceEndpoint: (endpoint: Endpoint) => void
targetEndpoints: Table<Endpoint>
targetEndpoints: IdMap<Endpoint>
addTargetEndpoint: (endpoint: Endpoint) => void
openedStepId?: string
setOpenedStepId: Dispatch<SetStateAction<string | undefined>>
@ -83,63 +82,55 @@ const graphContext = createContext<{
export const GraphProvider = ({
children,
typebot,
blocks,
isReadOnly = false,
}: {
children: ReactNode
typebot?: Typebot
blocks: Block[]
isReadOnly?: boolean
}) => {
const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue)
const [connectingIds, setConnectingIds] = useState<ConnectingIds | null>(null)
const [previewingEdgeId, setPreviewingEdgeId] = useState<string>()
const [sourceEndpoints, setSourceEndpoints] = useState<Table<Endpoint>>({
byId: {},
allIds: [],
})
const [targetEndpoints, setTargetEndpoints] = useState<Table<Endpoint>>({
byId: {},
allIds: [],
})
const [previewingEdge, setPreviewingEdge] = useState<Edge>()
const [sourceEndpoints, setSourceEndpoints] = useState<IdMap<Endpoint>>({})
const [targetEndpoints, setTargetEndpoints] = useState<IdMap<Endpoint>>({})
const [openedStepId, setOpenedStepId] = useState<string>()
const [blocksCoordinates, setBlocksCoordinates] = useImmer<
BlocksCoordinates | undefined
>(undefined)
const [blocksCoordinates, setBlocksCoordinates] = useState<BlocksCoordinates>(
{}
)
useEffect(() => {
setBlocksCoordinates(
typebot?.blocks.allIds.reduce(
(coords, blockId) => ({
byId: {
...coords.byId,
[blockId]: typebot.blocks.byId[blockId].graphCoordinates,
},
blocks.reduce(
(coords, block) => ({
...coords,
[block.id]: block.graphCoordinates,
}),
{ byId: {} }
{}
)
)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [typebot?.blocks])
}, [blocks])
const addSourceEndpoint = (endpoint: Endpoint) => {
setSourceEndpoints((endpoints) => ({
byId: { ...endpoints.byId, [endpoint.id]: endpoint },
allIds: [...endpoints.allIds, endpoint.id],
...endpoints,
[endpoint.id]: endpoint,
}))
}
const addTargetEndpoint = (endpoint: Endpoint) => {
setTargetEndpoints((endpoints) => ({
byId: { ...endpoints.byId, [endpoint.id]: endpoint },
allIds: [...endpoints.allIds, endpoint.id],
...endpoints,
[endpoint.id]: endpoint,
}))
}
const updateBlockCoordinates = (blockId: string, newCoord: Coordinates) =>
setBlocksCoordinates((blocksCoordinates) => {
if (!blocksCoordinates) return
blocksCoordinates.byId[blockId] = newCoord
})
setBlocksCoordinates((blocksCoordinates) => ({
...blocksCoordinates,
[blockId]: newCoord,
}))
return (
<graphContext.Provider
@ -148,8 +139,8 @@ export const GraphProvider = ({
setGraphPosition,
connectingIds,
setConnectingIds,
previewingEdgeId,
setPreviewingEdgeId,
previewingEdge,
setPreviewingEdge,
sourceEndpoints,
targetEndpoints,
addSourceEndpoint,

View File

@ -0,0 +1,125 @@
import { useEventListener } from '@chakra-ui/react'
import { ButtonItem, DraggableStep, DraggableStepType } from 'models'
import {
createContext,
Dispatch,
ReactNode,
SetStateAction,
useContext,
useRef,
useState,
} from 'react'
import { Coordinates } from './GraphContext'
type BlockInfo = {
id: string
ref: React.MutableRefObject<HTMLDivElement | null>
}
const graphDndContext = createContext<{
draggedStepType?: DraggableStepType
setDraggedStepType: Dispatch<SetStateAction<DraggableStepType | undefined>>
draggedStep?: DraggableStep
setDraggedStep: Dispatch<SetStateAction<DraggableStep | undefined>>
draggedItem?: ButtonItem
setDraggedItem: Dispatch<SetStateAction<ButtonItem | undefined>>
mouseOverBlock?: BlockInfo
setMouseOverBlock: Dispatch<SetStateAction<BlockInfo | undefined>>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})
export type NodePosition = { absolute: Coordinates; relative: Coordinates }
export const GraphDndContext = ({ children }: { children: ReactNode }) => {
const [draggedStep, setDraggedStep] = useState<DraggableStep>()
const [draggedStepType, setDraggedStepType] = useState<
DraggableStepType | undefined
>()
const [draggedItem, setDraggedItem] = useState<ButtonItem | undefined>()
const [mouseOverBlock, setMouseOverBlock] = useState<BlockInfo>()
return (
<graphDndContext.Provider
value={{
draggedStep,
setDraggedStep,
draggedStepType,
setDraggedStepType,
draggedItem,
setDraggedItem,
mouseOverBlock,
setMouseOverBlock,
}}
>
{children}
</graphDndContext.Provider>
)
}
export const useDragDistance = ({
ref,
onDrag,
distanceTolerance = 20,
isDisabled = false,
}: {
ref: React.MutableRefObject<HTMLDivElement | null>
onDrag: (position: { absolute: Coordinates; relative: Coordinates }) => void
distanceTolerance?: number
isDisabled: boolean
}) => {
const mouseDownPosition =
useRef<{ absolute: Coordinates; relative: Coordinates }>()
const handleMouseUp = () => {
if (mouseDownPosition) mouseDownPosition.current = undefined
}
useEventListener('mouseup', handleMouseUp)
const handleMouseDown = (e: MouseEvent) => {
if (isDisabled || !ref.current) return
e.stopPropagation()
const { top, left } = ref.current.getBoundingClientRect()
mouseDownPosition.current = {
absolute: { x: e.clientX, y: e.clientY },
relative: {
x: e.clientX - left,
y: e.clientY - top,
},
}
}
useEventListener('mousedown', handleMouseDown, ref.current)
const handleMouseMove = (e: MouseEvent) => {
if (!mouseDownPosition.current) return
const { clientX, clientY } = e
if (
Math.abs(mouseDownPosition.current.absolute.x - clientX) >
distanceTolerance ||
Math.abs(mouseDownPosition.current.absolute.y - clientY) >
distanceTolerance
) {
onDrag(mouseDownPosition.current)
}
}
useEventListener('mousemove', handleMouseMove)
}
export const computeNearestPlaceholderIndex = (
offsetY: number,
placeholderRefs: React.MutableRefObject<HTMLDivElement[]>
) => {
const { closestIndex } = placeholderRefs.current.reduce(
(prev, elem, index) => {
const elementTop = elem.getBoundingClientRect().top
const mouseDistanceFromPlaceholder = Math.abs(offsetY - elementTop)
return mouseDistanceFromPlaceholder < prev.value
? { closestIndex: index, value: mouseDistanceFromPlaceholder }
: prev
},
{ closestIndex: 0, value: 100 }
)
return closestIndex
}
export const useStepDnd = () => useContext(graphDndContext)

View File

@ -1,52 +0,0 @@
import { ChoiceItem, DraggableStep, DraggableStepType } from 'models'
import {
createContext,
Dispatch,
ReactNode,
SetStateAction,
useContext,
useState,
} from 'react'
const stepDndContext = createContext<{
draggedStepType?: DraggableStepType
setDraggedStepType: Dispatch<SetStateAction<DraggableStepType | undefined>>
draggedStep?: DraggableStep
setDraggedStep: Dispatch<SetStateAction<DraggableStep | undefined>>
draggedChoiceItem?: ChoiceItem
setDraggedChoiceItem: Dispatch<SetStateAction<ChoiceItem | undefined>>
mouseOverBlockId?: string
setMouseOverBlockId: Dispatch<SetStateAction<string | undefined>>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})
export const StepDndContext = ({ children }: { children: ReactNode }) => {
const [draggedStep, setDraggedStep] = useState<DraggableStep>()
const [draggedStepType, setDraggedStepType] = useState<
DraggableStepType | undefined
>()
const [draggedChoiceItem, setDraggedChoiceItem] = useState<
ChoiceItem | undefined
>()
const [mouseOverBlockId, setMouseOverBlockId] = useState<string>()
return (
<stepDndContext.Provider
value={{
draggedStep,
setDraggedStep,
draggedStepType,
setDraggedStepType,
draggedChoiceItem,
setDraggedChoiceItem,
mouseOverBlockId,
setMouseOverBlockId,
}}
>
{children}
</stepDndContext.Provider>
)
}
export const useStepDnd = () => useContext(stepDndContext)

View File

@ -25,13 +25,12 @@ import useSWR from 'swr'
import { isDefined } from 'utils'
import { BlocksActions, blocksActions } from './actions/blocks'
import { stepsAction, StepsActions } from './actions/steps'
import { choiceItemsAction, ChoiceItemsActions } from './actions/choiceItems'
import { variablesAction, VariablesActions } from './actions/variables'
import { edgesAction, EdgesActions } from './actions/edges'
import { webhooksAction, WebhooksAction } from './actions/webhooks'
import { useRegisterActions } from 'kbar'
import useUndo from 'services/utils/useUndo'
import { useDebounce } from 'use-debounce'
import { itemsAction, ItemsActions } from './actions/items'
const autoSaveTimeout = 40000
type UpdateTypebotPayload = Partial<{
@ -59,10 +58,9 @@ const typebotContext = createContext<
publishTypebot: () => void
} & BlocksActions &
StepsActions &
ChoiceItemsActions &
ItemsActions &
VariablesActions &
EdgesActions &
WebhooksAction
EdgesActions
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
>({})
@ -72,7 +70,7 @@ export const TypebotContext = ({
typebotId,
}: {
children: ReactNode
typebotId?: string
typebotId: string
}) => {
const router = useRouter()
const toast = useToast({
@ -237,10 +235,9 @@ export const TypebotContext = ({
updateTypebot: updateLocalTypebot,
...blocksActions(localTypebot as Typebot, setLocalTypebot),
...stepsAction(localTypebot as Typebot, setLocalTypebot),
...choiceItemsAction(localTypebot as Typebot, setLocalTypebot),
...variablesAction(localTypebot as Typebot, setLocalTypebot),
...edgesAction(localTypebot as Typebot, setLocalTypebot),
...webhooksAction(localTypebot as Typebot, setLocalTypebot),
...itemsAction(localTypebot as Typebot, setLocalTypebot),
}}
>
{children}
@ -254,13 +251,13 @@ export const useFetchedTypebot = ({
typebotId,
onError,
}: {
typebotId?: string
typebotId: string
onError: (error: Error) => void
}) => {
const { data, error, mutate } = useSWR<
{ typebot: Typebot; publishedTypebot?: PublicTypebot },
Error
>(typebotId ? `/api/typebots/${typebotId}` : null, fetcher)
>(`/api/typebots/${typebotId}`, fetcher)
if (error) onError(error)
return {
typebot: data?.typebot,

View File

@ -1,96 +1,86 @@
import { Coordinates } from 'contexts/GraphContext'
import { produce } from 'immer'
import { WritableDraft } from 'immer/dist/internal'
import { Block, DraggableStep, DraggableStepType, Typebot } from 'models'
import {
Block,
DraggableStep,
DraggableStepType,
StepIndices,
Typebot,
} from 'models'
import { SetTypebot } from '../TypebotContext'
import { deleteEdgeDraft } from './edges'
import { createStepDraft, deleteStepDraft } from './steps'
import { cleanUpEdgeDraft } from './edges'
import { createStepDraft } from './steps'
export type BlocksActions = {
createBlock: (
props: Coordinates & {
id: string
step: DraggableStep | DraggableStepType
indices: StepIndices
}
) => void
updateBlock: (blockId: string, updates: Partial<Omit<Block, 'id'>>) => void
deleteBlock: (blockId: string) => void
updateBlock: (blockIndex: number, updates: Partial<Omit<Block, 'id'>>) => void
deleteBlock: (blockIndex: number) => void
}
export const blocksActions = (
const blocksActions = (
typebot: Typebot,
setTypebot: SetTypebot
): BlocksActions => ({
createBlock: ({
id,
step,
indices,
...graphCoordinates
}: Coordinates & {
id: string
step: DraggableStep | DraggableStepType
indices: StepIndices
}) => {
setTypebot(
produce(typebot, (typebot) => {
const newBlock: Block = {
id,
graphCoordinates,
title: `Block ${typebot.blocks.allIds.length}`,
stepIds: [],
title: `Block #${typebot.blocks.length}`,
steps: [],
}
typebot.blocks.byId[newBlock.id] = newBlock
typebot.blocks.allIds.push(newBlock.id)
createStepDraft(typebot, step, newBlock.id)
removeEmptyBlocks(typebot)
typebot.blocks.push(newBlock)
createStepDraft(typebot, step, newBlock.id, indices)
})
)
},
updateBlock: (blockId: string, updates: Partial<Omit<Block, 'id'>>) =>
updateBlock: (blockIndex: number, updates: Partial<Omit<Block, 'id'>>) =>
setTypebot(
produce(typebot, (typebot) => {
typebot.blocks.byId[blockId] = {
...typebot.blocks.byId[blockId],
...updates,
}
const block = typebot.blocks[blockIndex]
typebot.blocks[blockIndex] = { ...block, ...updates }
})
),
deleteBlock: (blockId: string) =>
deleteBlock: (blockIndex: number) =>
setTypebot(
produce(typebot, (typebot) => {
deleteStepsInsideBlock(typebot, blockId)
deleteAssociatedEdges(typebot, blockId)
deleteBlockDraft(typebot)(blockId)
deleteBlockDraft(typebot)(blockIndex)
})
),
})
export const removeEmptyBlocks = (typebot: WritableDraft<Typebot>) => {
const emptyBlockIds = typebot.blocks.allIds.filter(
(blockId) => typebot.blocks.byId[blockId].stepIds.length === 0
)
emptyBlockIds.forEach(deleteBlockDraft(typebot))
}
const deleteAssociatedEdges = (
typebot: WritableDraft<Typebot>,
blockId: string
) => {
typebot.edges.allIds.forEach((edgeId) => {
if (typebot.edges.byId[edgeId].to.blockId === blockId)
deleteEdgeDraft(typebot, edgeId)
})
}
const deleteStepsInsideBlock = (
typebot: WritableDraft<Typebot>,
blockId: string
) => {
const block = typebot.blocks.byId[blockId]
block.stepIds.forEach((stepId) => deleteStepDraft(stepId)(typebot))
}
export const deleteBlockDraft =
(typebot: WritableDraft<Typebot>) => (blockId: string) => {
delete typebot.blocks.byId[blockId]
const index = typebot.blocks.allIds.indexOf(blockId)
if (index !== -1) typebot.blocks.allIds.splice(index, 1)
const deleteBlockDraft =
(typebot: WritableDraft<Typebot>) => (blockIndex: number) => {
cleanUpEdgeDraft(typebot, typebot.blocks[blockIndex].id)
typebot.blocks.splice(blockIndex, 1)
}
const removeEmptyBlocks = (typebot: WritableDraft<Typebot>) => {
const emptyBlocksIndices = typebot.blocks.reduce<number[]>(
(arr, block, idx) => {
block.steps.length === 0 && arr.push(idx)
return arr
},
[]
)
emptyBlocksIndices.forEach(deleteBlockDraft(typebot))
}
export { blocksActions, removeEmptyBlocks }

View File

@ -1,85 +0,0 @@
import { ChoiceItem, InputStepType, Typebot } from 'models'
import { generate } from 'short-uuid'
import assert from 'assert'
import { SetTypebot } from '../TypebotContext'
import { produce } from 'immer'
import { WritableDraft } from 'immer/dist/internal'
export type ChoiceItemsActions = {
createChoiceItem: (
item: ChoiceItem | Pick<ChoiceItem, 'stepId'>,
index?: number
) => void
updateChoiceItem: (
itemId: string,
updates: Partial<Omit<ChoiceItem, 'id'>>
) => void
deleteChoiceItem: (itemId: string) => void
}
export const choiceItemsAction = (
typebot: Typebot,
setTypebot: SetTypebot
): ChoiceItemsActions => ({
createChoiceItem: (
item: ChoiceItem | Pick<ChoiceItem, 'stepId'>,
index?: number
) => {
setTypebot(
produce(typebot, (typebot) => {
createChoiceItemDraft(typebot, item, index)
})
)
},
updateChoiceItem: (
itemId: string,
updates: Partial<Omit<ChoiceItem, 'id'>>
) =>
setTypebot(
produce(typebot, (typebot) => {
typebot.choiceItems.byId[itemId] = {
...typebot.choiceItems.byId[itemId],
...updates,
}
})
),
deleteChoiceItem: (itemId: string) => {
setTypebot(
produce(typebot, (typebot) => {
removeChoiceItemFromStep(typebot, itemId)
deleteChoiceItemDraft(typebot, itemId)
})
)
},
})
const removeChoiceItemFromStep = (
typebot: WritableDraft<Typebot>,
itemId: string
) => {
const containerStepId = typebot.choiceItems.byId[itemId].stepId
const step = typebot.steps.byId[containerStepId]
assert(step.type === InputStepType.CHOICE)
step.options?.itemIds.splice(step.options.itemIds.indexOf(itemId), 1)
}
export const deleteChoiceItemDraft = (typebot: Typebot, itemId: string) => {
delete typebot.choiceItems.byId[itemId]
const index = typebot.choiceItems.allIds.indexOf(itemId)
if (index !== -1) typebot.choiceItems.allIds.splice(index, 1)
}
export const createChoiceItemDraft = (
typebot: Typebot,
item: ChoiceItem | Pick<ChoiceItem, 'stepId'>,
index?: number
) => {
const step = typebot.steps.byId[item.stepId]
assert(step.type === InputStepType.CHOICE)
const newItem: ChoiceItem =
'id' in item ? { ...item } : { id: generate(), stepId: item.stepId }
typebot.choiceItems.byId[newItem.id] = newItem
typebot.choiceItems.allIds.push(newItem.id)
if (step.options.itemIds.indexOf(newItem.id) !== -1) return
step.options.itemIds.splice(index ?? 0, 0, newItem.id)
}

View File

@ -1,12 +1,13 @@
import { Typebot, Edge, ConditionStep } from 'models'
import { Typebot, Edge, StepWithItems, StepIndices, ItemIndices } from 'models'
import { WritableDraft } from 'immer/dist/types/types-external'
import { generate } from 'short-uuid'
import { SetTypebot } from '../TypebotContext'
import { produce } from 'immer'
import { byId, isDefined, isNotDefined } from 'utils'
export type EdgesActions = {
createEdge: (edge: Omit<Edge, 'id'>) => void
updateEdge: (edgeId: string, updates: Partial<Omit<Edge, 'id'>>) => void
updateEdge: (edgeIndex: number, updates: Partial<Omit<Edge, 'id'>>) => void
deleteEdge: (edgeId: string) => void
}
@ -21,40 +22,37 @@ export const edgesAction = (
...edge,
id: generate(),
}
if (edge.from.buttonId) {
deleteEdgeDraft(
typebot,
typebot.choiceItems.byId[edge.from.buttonId].edgeId
)
typebot.choiceItems.byId[edge.from.buttonId].edgeId = newEdge.id
} else if (edge.from.conditionType === 'true') {
deleteEdgeDraft(
typebot,
(typebot.steps.byId[edge.from.stepId] as ConditionStep).trueEdgeId
)
;(typebot.steps.byId[edge.from.stepId] as ConditionStep).trueEdgeId =
newEdge.id
} else if (edge.from.conditionType === 'false') {
deleteEdgeDraft(
typebot,
(typebot.steps.byId[edge.from.stepId] as ConditionStep).falseEdgeId
)
;(typebot.steps.byId[edge.from.stepId] as ConditionStep).falseEdgeId =
newEdge.id
} else {
deleteEdgeDraft(typebot, typebot.steps.byId[edge.from.stepId].edgeId)
typebot.steps.byId[edge.from.stepId].edgeId = newEdge.id
}
typebot.edges.byId[newEdge.id] = newEdge
typebot.edges.allIds.push(newEdge.id)
removeExistingEdge(typebot, edge)
typebot.edges.push(newEdge)
const blockIndex = typebot.blocks.findIndex(byId(edge.from.blockId))
const stepIndex = typebot.blocks[blockIndex].steps.findIndex(
byId(edge.from.stepId)
)
const itemIndex = edge.from.itemId
? (
typebot.blocks[blockIndex].steps[stepIndex] as StepWithItems
).items.findIndex(byId(edge.from.itemId))
: null
isDefined(itemIndex)
? addEdgeIdToItem(typebot, newEdge.id, {
blockIndex,
stepIndex,
itemIndex,
})
: addEdgeIdToStep(typebot, newEdge.id, {
blockIndex,
stepIndex,
})
})
)
},
updateEdge: (edgeId: string, updates: Partial<Omit<Edge, 'id'>>) =>
updateEdge: (edgeIndex: number, updates: Partial<Omit<Edge, 'id'>>) =>
setTypebot(
produce(typebot, (typebot) => {
typebot.edges.byId[edgeId] = {
...typebot.edges.byId[edgeId],
const currentEdge = typebot.edges[edgeIndex]
typebot.edges[edgeIndex] = {
...currentEdge,
...updates,
}
})
@ -68,12 +66,55 @@ export const edgesAction = (
},
})
const addEdgeIdToStep = (
typebot: WritableDraft<Typebot>,
edgeId: string,
{ blockIndex, stepIndex }: StepIndices
) => {
typebot.blocks[blockIndex].steps[stepIndex].outgoingEdgeId = edgeId
}
const addEdgeIdToItem = (
typebot: WritableDraft<Typebot>,
edgeId: string,
{ blockIndex, stepIndex, itemIndex }: ItemIndices
) => {
;(typebot.blocks[blockIndex].steps[stepIndex] as StepWithItems).items[
itemIndex
].outgoingEdgeId = edgeId
}
export const deleteEdgeDraft = (
typebot: WritableDraft<Typebot>,
edgeId?: string
edgeId: string
) => {
if (!edgeId) return
delete typebot.edges.byId[edgeId]
const index = typebot.edges.allIds.indexOf(edgeId)
if (index !== -1) typebot.edges.allIds.splice(index, 1)
const edgeIndex = typebot.edges.findIndex(byId(edgeId))
typebot.edges.splice(edgeIndex, 1)
}
export const cleanUpEdgeDraft = (
typebot: WritableDraft<Typebot>,
deletedNodeId: string
) => {
typebot.edges = typebot.edges.filter(
(edge) =>
![
edge.from.blockId,
edge.from.stepId,
edge.from.itemId,
edge.to.blockId,
edge.to.stepId,
].includes(deletedNodeId)
)
}
const removeExistingEdge = (
typebot: WritableDraft<Typebot>,
edge: Omit<Edge, 'id'>
) => {
typebot.edges = typebot.edges.filter((e) =>
edge.from.itemId
? e.from.itemId !== edge.from.itemId
: isDefined(e.from.itemId) || e.from.stepId !== edge.from.stepId
)
}

View File

@ -0,0 +1,71 @@
import {
Typebot,
ItemIndices,
Item,
InputStepType,
StepWithItems,
ButtonItem,
} from 'models'
import { SetTypebot } from '../TypebotContext'
import produce from 'immer'
import { cleanUpEdgeDraft } from './edges'
import { stepHasItems } from 'utils'
import { generate } from 'short-uuid'
export type ItemsActions = {
createItem: (item: Omit<ButtonItem, 'id'>, indices: ItemIndices) => void
updateItem: (indices: ItemIndices, updates: Partial<Omit<Item, 'id'>>) => void
deleteItem: (indices: ItemIndices) => void
}
const itemsAction = (
typebot: Typebot,
setTypebot: SetTypebot
): ItemsActions => ({
createItem: (
item: Omit<ButtonItem, 'id'>,
{ blockIndex, stepIndex, itemIndex }: ItemIndices
) => {
setTypebot(
produce(typebot, (typebot) => {
const step = typebot.blocks[blockIndex].steps[stepIndex]
if (step.type !== InputStepType.CHOICE) return
step.items.splice(itemIndex, 0, {
...item,
stepId: step.id,
id: generate(),
})
})
)
},
updateItem: (
{ blockIndex, stepIndex, itemIndex }: ItemIndices,
updates: Partial<Omit<Item, 'id'>>
) =>
setTypebot(
produce(typebot, (typebot) => {
const step = typebot.blocks[blockIndex].steps[stepIndex]
if (!stepHasItems(step)) return
;(typebot.blocks[blockIndex].steps[stepIndex] as StepWithItems).items[
itemIndex
] = {
...step.items[itemIndex],
...updates,
} as Item
})
),
deleteItem: ({ blockIndex, stepIndex, itemIndex }: ItemIndices) => {
setTypebot(
produce(typebot, (typebot) => {
const step = typebot.blocks[blockIndex].steps[
stepIndex
] as StepWithItems
const removingItem = step.items[itemIndex]
step.items.splice(itemIndex, 1)
cleanUpEdgeDraft(typebot, removingItem.id)
})
)
},
})
export { itemsAction }

View File

@ -1,142 +1,108 @@
import {
ChoiceInputStep,
Step,
Typebot,
DraggableStep,
DraggableStepType,
defaultWebhookAttributes,
StepIndices,
} from 'models'
import { parseNewStep } from 'services/typebots'
import { removeEmptyBlocks } from './blocks'
import { WritableDraft } from 'immer/dist/types/types-external'
import { createChoiceItemDraft, deleteChoiceItemDraft } from './choiceItems'
import { isChoiceInput, isWebhookStep } from 'utils'
import { deleteEdgeDraft } from './edges'
import { createWebhookDraft, deleteWebhookDraft } from './webhooks'
import { SetTypebot } from '../TypebotContext'
import produce from 'immer'
import { cleanUpEdgeDraft } from './edges'
export type StepsActions = {
createStep: (
blockId: string,
step?: DraggableStep | DraggableStepType,
index?: number
step: DraggableStep | DraggableStepType,
indices: StepIndices
) => void
updateStep: (
stepId: string,
indices: StepIndices,
updates: Partial<Omit<Step, 'id' | 'type'>>
) => void
detachStepFromBlock: (stepId: string) => void
deleteStep: (stepId: string) => void
detachStepFromBlock: (indices: StepIndices) => void
deleteStep: (indices: StepIndices) => void
}
export const stepsAction = (
const stepsAction = (
typebot: Typebot,
setTypebot: SetTypebot
): StepsActions => ({
createStep: (
blockId: string,
step?: DraggableStep | DraggableStepType,
index?: number
step: DraggableStep | DraggableStepType,
indices: StepIndices
) => {
if (!step) return
setTypebot(
produce(typebot, (typebot) => {
createStepDraft(typebot, step, blockId, index)
createStepDraft(typebot, step, blockId, indices)
})
)
},
updateStep: (
{ blockIndex, stepIndex }: StepIndices,
updates: Partial<Omit<Step, 'id' | 'type'>>
) =>
setTypebot(
produce(typebot, (typebot) => {
const step = typebot.blocks[blockIndex].steps[stepIndex]
typebot.blocks[blockIndex].steps[stepIndex] = { ...step, ...updates }
})
),
detachStepFromBlock: (indices: StepIndices) => {
setTypebot(produce(typebot, removeStepFromBlock(indices)))
},
deleteStep: (indices: StepIndices) => {
setTypebot(
produce(typebot, (typebot) => {
removeStepFromBlock(indices)(typebot)
removeEmptyBlocks(typebot)
})
)
},
updateStep: (stepId: string, updates: Partial<Omit<Step, 'id' | 'type'>>) =>
setTypebot(
produce(typebot, (typebot) => {
typebot.steps.byId[stepId] = {
...typebot.steps.byId[stepId],
...updates,
}
})
),
detachStepFromBlock: (stepId: string) => {
setTypebot(
produce(typebot, (typebot) => {
removeStepIdFromBlock(typebot, stepId)
})
)
},
deleteStep: (stepId: string) => {
setTypebot(produce(typebot, deleteStepDraft(stepId)))
},
})
const removeStepIdFromBlock = (
typebot: WritableDraft<Typebot>,
stepId: string
) => {
const containerBlock = typebot.blocks.byId[typebot.steps.byId[stepId].blockId]
containerBlock.stepIds.splice(containerBlock.stepIds.indexOf(stepId), 1)
}
export const deleteStepDraft =
(stepId: string) => (typebot: WritableDraft<Typebot>) => {
const step = typebot.steps.byId[stepId]
if (isChoiceInput(step)) deleteChoiceItemsInsideStep(typebot, step)
if (isWebhookStep(step))
deleteWebhookDraft(step.options?.webhookId)(typebot)
deleteEdgeDraft(typebot, step.edgeId)
removeStepIdFromBlock(typebot, stepId)
delete typebot.steps.byId[stepId]
const index = typebot.steps.allIds.indexOf(stepId)
if (index !== -1) typebot.steps.allIds.splice(index, 1)
removeEmptyBlocks(typebot)
const removeStepFromBlock =
({ blockIndex, stepIndex }: StepIndices) =>
(typebot: WritableDraft<Typebot>) => {
const removingStep = typebot.blocks[blockIndex].steps[stepIndex]
cleanUpEdgeDraft(typebot, removingStep.id)
typebot.blocks[blockIndex].steps.splice(stepIndex, 1)
}
export const createStepDraft = (
const createStepDraft = (
typebot: WritableDraft<Typebot>,
step: DraggableStep | DraggableStepType,
blockId: string,
index?: number
) =>
indices: StepIndices
) => {
typeof step === 'string'
? createNewStep(typebot, step, blockId, index)
: moveStepToBlock(typebot, step, blockId, index)
? createNewStep(typebot, step, blockId, indices)
: moveStepToBlock(typebot, step, blockId, indices)
removeEmptyBlocks(typebot)
}
const createNewStep = (
typebot: WritableDraft<Typebot>,
type: DraggableStepType,
blockId: string,
index?: number
{ blockIndex, stepIndex }: StepIndices
) => {
const newStep = parseNewStep(type, blockId)
typebot.steps.byId[newStep.id] = newStep
if (isChoiceInput(newStep)) {
createChoiceItemDraft(typebot, {
id: newStep.options.itemIds[0],
stepId: newStep.id,
})
} else if (isWebhookStep(newStep)) {
createWebhookDraft({
id: newStep.options.webhookId,
...defaultWebhookAttributes,
})(typebot)
}
typebot.steps.allIds.push(newStep.id)
typebot.blocks.byId[blockId].stepIds.splice(index ?? 0, 0, newStep.id)
typebot.blocks[blockIndex].steps.splice(stepIndex ?? 0, 0, newStep)
}
const moveStepToBlock = (
typebot: WritableDraft<Typebot>,
step: DraggableStep,
blockId: string,
index?: number
) => {
typebot.steps.byId[step.id].blockId = blockId
typebot.blocks.byId[blockId].stepIds.splice(index ?? 0, 0, step.id)
}
const deleteChoiceItemsInsideStep = (
typebot: WritableDraft<Typebot>,
step: ChoiceInputStep
{ blockIndex, stepIndex }: StepIndices
) =>
step.options?.itemIds.forEach((itemId) =>
deleteChoiceItemDraft(typebot, itemId)
)
typebot.blocks[blockIndex].steps.splice(stepIndex ?? 0, 0, {
...step,
blockId,
})
export { stepsAction, createStepDraft }

View File

@ -19,8 +19,7 @@ export const variablesAction = (
createVariable: (newVariable: Variable) => {
setTypebot(
produce(typebot, (typebot) => {
typebot.variables.byId[newVariable.id] = newVariable
typebot.variables.allIds.push(newVariable.id)
typebot.variables.push(newVariable)
})
)
},
@ -30,10 +29,9 @@ export const variablesAction = (
) =>
setTypebot(
produce(typebot, (typebot) => {
typebot.variables.byId[variableId] = {
...typebot.variables.byId[variableId],
...updates,
}
typebot.variables.map((v) =>
v.id === variableId ? { ...v, ...updates } : v
)
})
),
deleteVariable: (itemId: string) => {
@ -49,7 +47,6 @@ export const deleteVariableDraft = (
typebot: WritableDraft<Typebot>,
variableId: string
) => {
delete typebot.variables.byId[variableId]
const index = typebot.variables.allIds.indexOf(variableId)
if (index !== -1) typebot.variables.allIds.splice(index, 1)
const index = typebot.variables.findIndex((v) => v.id === variableId)
typebot.variables.splice(index, 1)
}

View File

@ -1,48 +0,0 @@
import { Typebot, Webhook } from 'models'
import { WritableDraft } from 'immer/dist/internal'
import { SetTypebot } from '../TypebotContext'
import { produce } from 'immer'
export type WebhooksAction = {
createWebhook: (webook: Webhook) => void
updateWebhook: (
webhookId: string,
updates: Partial<Omit<Webhook, 'id'>>
) => void
deleteWebhook: (variableId: string) => void
}
export const webhooksAction = (
typebot: Typebot,
setTypebot: SetTypebot
): WebhooksAction => ({
createWebhook: (newWebhook: Webhook) => {
setTypebot(produce(typebot, createWebhookDraft(newWebhook)))
},
updateWebhook: (webhookId: string, updates: Partial<Omit<Webhook, 'id'>>) =>
setTypebot(
produce(typebot, (typebot) => {
typebot.webhooks.byId[webhookId] = {
...typebot.webhooks.byId[webhookId],
...updates,
}
})
),
deleteWebhook: (webhookId: string) => {
setTypebot(produce(typebot, deleteWebhookDraft(webhookId)))
},
})
export const createWebhookDraft =
(newWebhook: Webhook) => (typebot: WritableDraft<Typebot>) => {
typebot.webhooks.byId[newWebhook.id] = newWebhook
typebot.webhooks.allIds.push(newWebhook.id)
}
export const deleteWebhookDraft =
(webhookId?: string) => (typebot: WritableDraft<Typebot>) => {
if (!webhookId) return
delete typebot.webhooks.byId[webhookId]
const index = typebot.webhooks.allIds.indexOf(webhookId)
if (index !== -1) typebot.webhooks.allIds.splice(index, 1)
}