refactor(editor): ♻️ Undo / Redo buttons + structure refacto
Yet another huge refacto... While implementing undo and redo features I understood that I updated the stored typebot too many times (i.e. on each key input) so I had to rethink it entirely. I also moved around some files.
This commit is contained in:
@ -1,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)
|
@ -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}
|
||||
|
@ -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])
|
||||
}
|
||||
|
@ -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 =
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -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>,
|
||||
|
@ -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)
|
||||
})
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 })
|
||||
|
Reference in New Issue
Block a user