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:
@ -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,
|
||||
|
125
apps/builder/contexts/GraphDndContext.tsx
Normal file
125
apps/builder/contexts/GraphDndContext.tsx
Normal 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)
|
@ -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)
|
@ -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,
|
||||
|
@ -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 }
|
||||
|
@ -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)
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
|
71
apps/builder/contexts/TypebotContext/actions/items.ts
Normal file
71
apps/builder/contexts/TypebotContext/actions/items.ts
Normal 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 }
|
@ -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 }
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
Reference in New Issue
Block a user