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,62 +0,0 @@
import { PublicTypebot } from 'models'
import {
createContext,
Dispatch,
ReactNode,
SetStateAction,
useContext,
useState,
} from 'react'
import { Coordinates } from './GraphContext'
import produce from 'immer'
type Position = Coordinates & { scale: number }
const graphPositionDefaultValue = { x: 400, y: 100, scale: 1 }
const graphContext = createContext<{
typebot?: PublicTypebot
updateBlockPosition: (blockId: string, newPositon: Coordinates) => void
graphPosition: Position
setGraphPosition: Dispatch<SetStateAction<Position>>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({
graphPosition: graphPositionDefaultValue,
})
export const AnalyticsGraphProvider = ({
children,
initialTypebot,
}: {
children: ReactNode
initialTypebot: PublicTypebot
}) => {
const [typebot, setTypebot] = useState(initialTypebot)
const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue)
const updateBlockPosition = (blockId: string, newPosition: Coordinates) => {
if (!typebot) return
setTypebot(
produce(typebot, (nextTypebot) => {
nextTypebot.blocks.byId[blockId].graphCoordinates = newPosition
})
)
}
return (
<graphContext.Provider
value={{
typebot,
graphPosition,
setGraphPosition,
updateBlockPosition,
}}
>
{children}
</graphContext.Provider>
)
}
export const useAnalyticsGraph = () => useContext(graphContext)

View File

@ -1,4 +1,4 @@
import { Block, Source, Step, Table, Target } from 'models'
import { Block, Source, Step, Table, Target, Typebot } from 'models'
import {
createContext,
Dispatch,
@ -6,8 +6,10 @@ import {
ReactNode,
SetStateAction,
useContext,
useEffect,
useState,
} from 'react'
import { useImmer } from 'use-immer'
export const stubLength = 20
export const blockWidth = 300
@ -48,13 +50,17 @@ export type ConnectingIds = {
}
type StepId = string
type NodeId = string
type ButtonId = string
export type Endpoint = {
id: StepId | NodeId
id: StepId | ButtonId
ref: MutableRefObject<HTMLDivElement | null>
}
export type BlocksCoordinates = { byId: { [key: string]: Coordinates } }
const graphContext = createContext<{
blocksCoordinates?: BlocksCoordinates
updateBlockCoordinates: (blockId: string, newCoord: Coordinates) => void
graphPosition: Position
setGraphPosition: Dispatch<SetStateAction<Position>>
connectingIds: ConnectingIds | null
@ -67,6 +73,7 @@ const graphContext = createContext<{
addTargetEndpoint: (endpoint: Endpoint) => void
openedStepId?: string
setOpenedStepId: Dispatch<SetStateAction<string | undefined>>
isReadOnly: boolean
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({
@ -74,7 +81,15 @@ const graphContext = createContext<{
connectingIds: null,
})
export const GraphProvider = ({ children }: { children: ReactNode }) => {
export const GraphProvider = ({
children,
typebot,
isReadOnly = false,
}: {
children: ReactNode
typebot?: Typebot
isReadOnly?: boolean
}) => {
const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue)
const [connectingIds, setConnectingIds] = useState<ConnectingIds | null>(null)
const [previewingEdgeId, setPreviewingEdgeId] = useState<string>()
@ -87,6 +102,24 @@ export const GraphProvider = ({ children }: { children: ReactNode }) => {
allIds: [],
})
const [openedStepId, setOpenedStepId] = useState<string>()
const [blocksCoordinates, setBlocksCoordinates] = useImmer<
BlocksCoordinates | undefined
>(undefined)
useEffect(() => {
setBlocksCoordinates(
typebot?.blocks.allIds.reduce(
(coords, blockId) => ({
byId: {
...coords.byId,
[blockId]: typebot.blocks.byId[blockId].graphCoordinates,
},
}),
{ byId: {} }
)
)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [typebot?.blocks])
const addSourceEndpoint = (endpoint: Endpoint) => {
setSourceEndpoints((endpoints) => ({
@ -102,6 +135,12 @@ export const GraphProvider = ({ children }: { children: ReactNode }) => {
}))
}
const updateBlockCoordinates = (blockId: string, newCoord: Coordinates) =>
setBlocksCoordinates((blocksCoordinates) => {
if (!blocksCoordinates) return
blocksCoordinates.byId[blockId] = newCoord
})
return (
<graphContext.Provider
value={{
@ -117,6 +156,9 @@ export const GraphProvider = ({ children }: { children: ReactNode }) => {
addTargetEndpoint,
openedStepId,
setOpenedStepId,
blocksCoordinates,
updateBlockCoordinates,
isReadOnly,
}}
>
{children}

View File

@ -24,14 +24,14 @@ import { fetcher, omit, preventUserFromRefreshing } from 'services/utils'
import useSWR from 'swr'
import { isDefined } from 'utils'
import { BlocksActions, blocksActions } from './actions/blocks'
import { useImmer, Updater } from 'use-immer'
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'
const autoSaveTimeout = 40000
type UpdateTypebotPayload = Partial<{
@ -40,6 +40,8 @@ type UpdateTypebotPayload = Partial<{
publicId: string
name: string
}>
export type SetTypebot = (typebot: Typebot | undefined) => void
const typebotContext = createContext<
{
typebot?: Typebot
@ -50,6 +52,9 @@ const typebotContext = createContext<
isSavingLoading: boolean
save: () => Promise<ToastId | undefined>
undo: () => void
redo: () => void
canRedo: boolean
canUndo: boolean
updateTypebot: (updates: UpdateTypebotPayload) => void
publishTypebot: () => void
} & BlocksActions &
@ -74,7 +79,6 @@ export const TypebotContext = ({
position: 'top-right',
status: 'error',
})
const [undoStack, setUndoStack] = useState<Typebot[]>([])
const { typebot, publishedTypebot, isLoading, mutate } = useFetchedTypebot({
typebotId,
onError: (error) =>
@ -84,20 +88,28 @@ export const TypebotContext = ({
}),
})
const [localTypebot, setLocalTypebot] = useImmer<Typebot | undefined>(
undefined
)
const [
{ present: localTypebot },
{
redo,
undo,
canRedo,
canUndo,
set: setLocalTypebot,
presentRef: currentTypebotRef,
},
] = useUndo<Typebot | undefined>(undefined)
const [debouncedLocalTypebot] = useDebounce(localTypebot, autoSaveTimeout)
useEffect(() => {
if (hasUnsavedChanges) saveTypebot()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedLocalTypebot])
const [localPublishedTypebot, setLocalPublishedTypebot] =
useState<PublicTypebot>()
const [isSavingLoading, setIsSavingLoading] = useState(false)
const [isPublishing, setIsPublishing] = useState(false)
const saveTypebot = async () => {
const typebotToSave = currentTypebotRef.current
if (!typebotToSave) return
setIsSavingLoading(true)
const { error } = await updateTypebot(typebotToSave.id, typebotToSave)
setIsSavingLoading(false)
if (error) return toast({ title: error.name, description: error.message })
mutate({ typebot: typebotToSave })
window.removeEventListener('beforeunload', preventUserFromRefreshing)
}
const hasUnsavedChanges = useMemo(
() =>
@ -107,6 +119,18 @@ export const TypebotContext = ({
[typebot, localTypebot]
)
useAutoSave({
handler: saveTypebot,
item: localTypebot,
canSave: hasUnsavedChanges,
debounceTimeout: autoSaveTimeout,
})
const [localPublishedTypebot, setLocalPublishedTypebot] =
useState<PublicTypebot>()
const [isSavingLoading, setIsSavingLoading] = useState(false)
const [isPublishing, setIsPublishing] = useState(false)
const isPublished = useMemo(
() =>
isDefined(typebot) &&
@ -117,8 +141,8 @@ export const TypebotContext = ({
useEffect(() => {
if (!localTypebot || !typebot) return
currentTypebotRef.current = localTypebot
if (!checkIfTypebotsAreEqual(localTypebot, typebot)) {
pushNewTypebotInUndoStack(localTypebot)
window.removeEventListener('beforeunload', preventUserFromRefreshing)
window.addEventListener('beforeunload', preventUserFromRefreshing)
} else {
@ -139,43 +163,30 @@ export const TypebotContext = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading])
const pushNewTypebotInUndoStack = (typebot: Typebot) => {
setUndoStack([...undoStack, typebot])
}
useRegisterActions(
[
{
id: 'save',
name: 'Save typebot',
perform: () => saveTypebot(),
},
],
[]
)
const undo = () => {
const lastTypebot = [...undoStack].pop()
setUndoStack(undoStack.slice(0, -1))
setLocalTypebot(lastTypebot)
}
useRegisterActions(
[
{
id: 'undo',
name: 'Undo changes',
perform: undo,
},
],
[localTypebot]
)
const saveTypebot = async (typebot?: Typebot) => {
if (!localTypebot) return
setIsSavingLoading(true)
const { error } = await updateTypebot(
typebot?.id ?? localTypebot.id,
typebot ?? localTypebot
)
setIsSavingLoading(false)
if (error) return toast({ title: error.name, description: error.message })
mutate({ typebot: typebot ?? localTypebot })
window.removeEventListener('beforeunload', preventUserFromRefreshing)
}
const updateLocalTypebot = ({
publicId,
settings,
theme,
name,
}: UpdateTypebotPayload) => {
setLocalTypebot((typebot) => {
if (!typebot) return
if (publicId) typebot.publicId = publicId
if (settings) typebot.settings = settings
if (theme) typebot.theme = theme
if (name) typebot.name = name
})
}
const updateLocalTypebot = (updates: UpdateTypebotPayload) =>
localTypebot && setLocalTypebot({ ...localTypebot, ...updates })
const publishTypebot = async () => {
if (!localTypebot) return
@ -188,8 +199,7 @@ export const TypebotContext = ({
updateLocalTypebot({ publicId: newPublicId })
newLocalTypebot.publicId = newPublicId
}
if (hasUnsavedChanges || !localPublishedTypebot)
await saveTypebot(newLocalTypebot)
if (hasUnsavedChanges || !localPublishedTypebot) await saveTypebot()
setIsPublishing(true)
if (localPublishedTypebot) {
const { error } = await updatePublishedTypebot(
@ -218,16 +228,19 @@ export const TypebotContext = ({
isSavingLoading,
save: saveTypebot,
undo,
redo,
canUndo,
canRedo,
publishTypebot,
isPublishing,
isPublished,
updateTypebot: updateLocalTypebot,
...blocksActions(setLocalTypebot as Updater<Typebot>),
...stepsAction(setLocalTypebot as Updater<Typebot>),
...choiceItemsAction(setLocalTypebot as Updater<Typebot>),
...variablesAction(setLocalTypebot as Updater<Typebot>),
...edgesAction(setLocalTypebot as Updater<Typebot>),
...webhooksAction(setLocalTypebot as Updater<Typebot>),
...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),
}}
>
{children}
@ -256,3 +269,22 @@ export const useFetchedTypebot = ({
mutate,
}
}
const useAutoSave = <T,>({
handler,
item,
canSave,
debounceTimeout,
}: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handler: (item?: T) => Promise<any>
item?: T
canSave: boolean
debounceTimeout: number
}) => {
const [debouncedItem] = useDebounce(item, debounceTimeout)
return useEffect(() => {
if (canSave) handler(item)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedItem])
}

View File

@ -1,14 +1,15 @@
import { Coordinates } from 'contexts/GraphContext'
import { produce } from 'immer'
import { WritableDraft } from 'immer/dist/internal'
import { Block, DraggableStep, DraggableStepType, Typebot } from 'models'
import { parseNewBlock } from 'services/typebots'
import { Updater } from 'use-immer'
import { SetTypebot } from '../TypebotContext'
import { deleteEdgeDraft } from './edges'
import { createStepDraft, deleteStepDraft } from './steps'
export type BlocksActions = {
createBlock: (
props: Coordinates & {
id: string
step: DraggableStep | DraggableStepType
}
) => void
@ -16,38 +17,50 @@ export type BlocksActions = {
deleteBlock: (blockId: string) => void
}
export const blocksActions = (setTypebot: Updater<Typebot>): BlocksActions => ({
export const blocksActions = (
typebot: Typebot,
setTypebot: SetTypebot
): BlocksActions => ({
createBlock: ({
x,
y,
id,
step,
...graphCoordinates
}: Coordinates & {
id: string
step: DraggableStep | DraggableStepType
}) => {
setTypebot((typebot) => {
const newBlock = parseNewBlock({
totalBlocks: typebot.blocks.allIds.length,
initialCoordinates: { x, y },
setTypebot(
produce(typebot, (typebot) => {
const newBlock: Block = {
id,
graphCoordinates,
title: `Block ${typebot.blocks.allIds.length}`,
stepIds: [],
}
typebot.blocks.byId[newBlock.id] = newBlock
typebot.blocks.allIds.push(newBlock.id)
createStepDraft(typebot, step, newBlock.id)
removeEmptyBlocks(typebot)
})
typebot.blocks.byId[newBlock.id] = newBlock
typebot.blocks.allIds.push(newBlock.id)
createStepDraft(typebot, step, newBlock.id)
removeEmptyBlocks(typebot)
})
)
},
updateBlock: (blockId: string, updates: Partial<Omit<Block, 'id'>>) =>
setTypebot((typebot) => {
typebot.blocks.byId[blockId] = {
...typebot.blocks.byId[blockId],
...updates,
}
}),
setTypebot(
produce(typebot, (typebot) => {
typebot.blocks.byId[blockId] = {
...typebot.blocks.byId[blockId],
...updates,
}
})
),
deleteBlock: (blockId: string) =>
setTypebot((typebot) => {
deleteStepsInsideBlock(typebot, blockId)
deleteAssociatedEdges(typebot, blockId)
deleteBlockDraft(typebot)(blockId)
}),
setTypebot(
produce(typebot, (typebot) => {
deleteStepsInsideBlock(typebot, blockId)
deleteAssociatedEdges(typebot, blockId)
deleteBlockDraft(typebot)(blockId)
})
),
})
export const removeEmptyBlocks = (typebot: WritableDraft<Typebot>) => {
@ -72,7 +85,7 @@ const deleteStepsInsideBlock = (
blockId: string
) => {
const block = typebot.blocks.byId[blockId]
block.stepIds.forEach((stepId) => deleteStepDraft(typebot, stepId))
block.stepIds.forEach((stepId) => deleteStepDraft(stepId)(typebot))
}
export const deleteBlockDraft =

View File

@ -1,8 +1,9 @@
import { ChoiceItem, InputStepType, Typebot } from 'models'
import { Updater } from 'use-immer'
import { WritableDraft } from 'immer/dist/types/types-external'
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: (
@ -17,31 +18,38 @@ export type ChoiceItemsActions = {
}
export const choiceItemsAction = (
setTypebot: Updater<Typebot>
typebot: Typebot,
setTypebot: SetTypebot
): ChoiceItemsActions => ({
createChoiceItem: (
item: ChoiceItem | Pick<ChoiceItem, 'stepId'>,
index?: number
) => {
setTypebot((typebot) => {
createChoiceItemDraft(typebot, item, index)
})
setTypebot(
produce(typebot, (typebot) => {
createChoiceItemDraft(typebot, item, index)
})
)
},
updateChoiceItem: (
itemId: string,
updates: Partial<Omit<ChoiceItem, 'id'>>
) =>
setTypebot((typebot) => {
typebot.choiceItems.byId[itemId] = {
...typebot.choiceItems.byId[itemId],
...updates,
}
}),
setTypebot(
produce(typebot, (typebot) => {
typebot.choiceItems.byId[itemId] = {
...typebot.choiceItems.byId[itemId],
...updates,
}
})
),
deleteChoiceItem: (itemId: string) => {
setTypebot((typebot) => {
removeChoiceItemFromStep(typebot, itemId)
deleteChoiceItemDraft(typebot, itemId)
})
setTypebot(
produce(typebot, (typebot) => {
removeChoiceItemFromStep(typebot, itemId)
deleteChoiceItemDraft(typebot, itemId)
})
)
},
})
@ -55,17 +63,14 @@ const removeChoiceItemFromStep = (
step.options?.itemIds.splice(step.options.itemIds.indexOf(itemId), 1)
}
export const deleteChoiceItemDraft = (
typebot: WritableDraft<Typebot>,
itemId: string
) => {
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: WritableDraft<Typebot>,
typebot: Typebot,
item: ChoiceItem | Pick<ChoiceItem, 'stepId'>,
index?: number
) => {
@ -75,5 +80,6 @@ export const createChoiceItemDraft = (
'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,7 +1,8 @@
import { Typebot, Edge, ConditionStep } from 'models'
import { Updater } from 'use-immer'
import { WritableDraft } from 'immer/dist/types/types-external'
import { generate } from 'short-uuid'
import { SetTypebot } from '../TypebotContext'
import { produce } from 'immer'
export type EdgesActions = {
createEdge: (edge: Omit<Edge, 'id'>) => void
@ -9,52 +10,61 @@ export type EdgesActions = {
deleteEdge: (edgeId: string) => void
}
export const edgesAction = (setTypebot: Updater<Typebot>): EdgesActions => ({
export const edgesAction = (
typebot: Typebot,
setTypebot: SetTypebot
): EdgesActions => ({
createEdge: (edge: Omit<Edge, 'id'>) => {
setTypebot((typebot) => {
const newEdge = {
...edge,
id: generate(),
}
if (edge.from.nodeId) {
deleteEdgeDraft(
typebot,
typebot.choiceItems.byId[edge.from.nodeId].edgeId
)
typebot.choiceItems.byId[edge.from.nodeId].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)
})
setTypebot(
produce(typebot, (typebot) => {
const newEdge = {
...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)
})
)
},
updateEdge: (edgeId: string, updates: Partial<Omit<Edge, 'id'>>) =>
setTypebot((typebot) => {
typebot.edges.byId[edgeId] = {
...typebot.edges.byId[edgeId],
...updates,
}
}),
setTypebot(
produce(typebot, (typebot) => {
typebot.edges.byId[edgeId] = {
...typebot.edges.byId[edgeId],
...updates,
}
})
),
deleteEdge: (edgeId: string) => {
setTypebot((typebot) => {
deleteEdgeDraft(typebot, edgeId)
})
setTypebot(
produce(typebot, (typebot) => {
deleteEdgeDraft(typebot, edgeId)
})
)
},
})

View File

@ -4,15 +4,17 @@ import {
Typebot,
DraggableStep,
DraggableStepType,
defaultWebhookAttributes,
} from 'models'
import { parseNewStep } from 'services/typebots'
import { Updater } from 'use-immer'
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 { deleteWebhookDraft } from './webhooks'
import { createWebhookDraft, deleteWebhookDraft } from './webhooks'
import { SetTypebot } from '../TypebotContext'
import produce from 'immer'
export type StepsActions = {
createStep: (
@ -28,37 +30,41 @@ export type StepsActions = {
deleteStep: (stepId: string) => void
}
export const stepsAction = (setTypebot: Updater<Typebot>): StepsActions => ({
export const stepsAction = (
typebot: Typebot,
setTypebot: SetTypebot
): StepsActions => ({
createStep: (
blockId: string,
step?: DraggableStep | DraggableStepType,
index?: number
) => {
if (!step) return
setTypebot((typebot) => {
createStepDraft(typebot, step, blockId, index)
removeEmptyBlocks(typebot)
})
setTypebot(
produce(typebot, (typebot) => {
createStepDraft(typebot, step, blockId, index)
removeEmptyBlocks(typebot)
})
)
},
updateStep: (stepId: string, updates: Partial<Omit<Step, 'id' | 'type'>>) =>
setTypebot((typebot) => {
typebot.steps.byId[stepId] = { ...typebot.steps.byId[stepId], ...updates }
}),
setTypebot(
produce(typebot, (typebot) => {
typebot.steps.byId[stepId] = {
...typebot.steps.byId[stepId],
...updates,
}
})
),
detachStepFromBlock: (stepId: string) => {
setTypebot((typebot) => {
removeStepIdFromBlock(typebot, stepId)
})
setTypebot(
produce(typebot, (typebot) => {
removeStepIdFromBlock(typebot, stepId)
})
)
},
deleteStep: (stepId: string) => {
setTypebot((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)
deleteStepDraft(typebot, stepId)
})
setTypebot(produce(typebot, deleteStepDraft(stepId)))
},
})
@ -70,14 +76,19 @@ const removeStepIdFromBlock = (
containerBlock.stepIds.splice(containerBlock.stepIds.indexOf(stepId), 1)
}
export const deleteStepDraft = (
typebot: WritableDraft<Typebot>,
stepId: string
) => {
delete typebot.steps.byId[stepId]
const index = typebot.steps.allIds.indexOf(stepId)
if (index !== -1) typebot.steps.allIds.splice(index, 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)
}
export const createStepDraft = (
typebot: WritableDraft<Typebot>,
@ -97,8 +108,17 @@ const createNewStep = (
) => {
const newStep = parseNewStep(type, blockId)
typebot.steps.byId[newStep.id] = newStep
if (isChoiceInput(newStep))
createChoiceItemDraft(typebot, { stepId: newStep.id })
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)
}
@ -108,7 +128,10 @@ const moveStepToBlock = (
step: DraggableStep,
blockId: string,
index?: number
) => typebot.blocks.byId[blockId].stepIds.splice(index ?? 0, 0, step.id)
) => {
typebot.steps.byId[step.id].blockId = blockId
typebot.blocks.byId[blockId].stepIds.splice(index ?? 0, 0, step.id)
}
const deleteChoiceItemsInsideStep = (
typebot: WritableDraft<Typebot>,

View File

@ -1,6 +1,7 @@
import { Typebot, Variable } from 'models'
import { Updater } from 'use-immer'
import { WritableDraft } from 'immer/dist/types/types-external'
import { SetTypebot } from '../TypebotContext'
import { produce } from 'immer'
export type VariablesActions = {
createVariable: (variable: Variable) => void
@ -12,28 +13,35 @@ export type VariablesActions = {
}
export const variablesAction = (
setTypebot: Updater<Typebot>
typebot: Typebot,
setTypebot: SetTypebot
): VariablesActions => ({
createVariable: (newVariable: Variable) => {
setTypebot((typebot) => {
typebot.variables.byId[newVariable.id] = newVariable
typebot.variables.allIds.push(newVariable.id)
})
setTypebot(
produce(typebot, (typebot) => {
typebot.variables.byId[newVariable.id] = newVariable
typebot.variables.allIds.push(newVariable.id)
})
)
},
updateVariable: (
variableId: string,
updates: Partial<Omit<Variable, 'id'>>
) =>
setTypebot((typebot) => {
typebot.variables.byId[variableId] = {
...typebot.variables.byId[variableId],
...updates,
}
}),
setTypebot(
produce(typebot, (typebot) => {
typebot.variables.byId[variableId] = {
...typebot.variables.byId[variableId],
...updates,
}
})
),
deleteVariable: (itemId: string) => {
setTypebot((typebot) => {
deleteVariableDraft(typebot, itemId)
})
setTypebot(
produce(typebot, (typebot) => {
deleteVariableDraft(typebot, itemId)
})
)
},
})

View File

@ -1,6 +1,7 @@
import { Typebot, Webhook } from 'models'
import { Updater } from 'use-immer'
import { WritableDraft } from 'immer/dist/internal'
import { SetTypebot } from '../TypebotContext'
import { produce } from 'immer'
export type WebhooksAction = {
createWebhook: (webook: Webhook) => void
@ -12,26 +13,32 @@ export type WebhooksAction = {
}
export const webhooksAction = (
setTypebot: Updater<Typebot>
typebot: Typebot,
setTypebot: SetTypebot
): WebhooksAction => ({
createWebhook: (newWebhook: Webhook) => {
setTypebot((typebot) => {
typebot.webhooks.byId[newWebhook.id] = newWebhook
typebot.webhooks.allIds.push(newWebhook.id)
})
setTypebot(produce(typebot, createWebhookDraft(newWebhook)))
},
updateWebhook: (webhookId: string, updates: Partial<Omit<Webhook, 'id'>>) =>
setTypebot((typebot) => {
typebot.webhooks.byId[webhookId] = {
...typebot.webhooks.byId[webhookId],
...updates,
}
}),
setTypebot(
produce(typebot, (typebot) => {
typebot.webhooks.byId[webhookId] = {
...typebot.webhooks.byId[webhookId],
...updates,
}
})
),
deleteWebhook: (webhookId: string) => {
setTypebot(deleteWebhookDraft(webhookId))
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

View File

@ -8,7 +8,7 @@ import {
useMemo,
useState,
} from 'react'
import { isDefined } from 'utils'
import { isDefined, isNotDefined } from 'utils'
import { updateUser as updateUserInDb } from 'services/user'
import { useToast } from '@chakra-ui/react'
import { deepEqual } from 'fast-equals'
@ -54,7 +54,7 @@ export const UserContext = ({ children }: { children: ReactNode }) => {
})
useEffect(() => {
if (isDefined(user) || !isDefined(session)) return
if (isDefined(user) || isNotDefined(session)) return
setUser(session.user as User)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [session])
@ -70,12 +70,12 @@ export const UserContext = ({ children }: { children: ReactNode }) => {
const isSigningIn = () => ['/signin', '/register'].includes(router.pathname)
const updateUser = (newUser: Partial<User>) => {
if (!isDefined(user)) return
if (isNotDefined(user)) return
setUser({ ...user, ...newUser })
}
const saveUser = async () => {
if (!isDefined(user)) return
if (isNotDefined(user)) return
setIsSaving(true)
const { error } = await updateUserInDb(user.id, user)
if (error) toast({ title: error.name, description: error.message })