♻️ Normalize data
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import { Block, PublicTypebot } from 'bot-engine'
|
||||
import { PublicTypebot } from 'models'
|
||||
import {
|
||||
createContext,
|
||||
Dispatch,
|
||||
@ -8,6 +8,7 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { Coordinates } from './GraphContext'
|
||||
import produce from 'immer'
|
||||
|
||||
type Position = Coordinates & { scale: number }
|
||||
|
||||
@ -35,31 +36,13 @@ export const AnalyticsGraphProvider = ({
|
||||
|
||||
const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue)
|
||||
|
||||
const updateBlocks = (blocks: Block[]) => {
|
||||
if (!typebot) return
|
||||
setTypebot({
|
||||
...typebot,
|
||||
blocks: [...blocks],
|
||||
})
|
||||
}
|
||||
|
||||
const updateBlockPosition = (blockId: string, newPosition: Coordinates) => {
|
||||
if (!typebot) return
|
||||
blockId === 'start-block'
|
||||
? setTypebot({
|
||||
...typebot,
|
||||
startBlock: {
|
||||
...typebot.startBlock,
|
||||
graphCoordinates: newPosition,
|
||||
},
|
||||
})
|
||||
: updateBlocks(
|
||||
typebot.blocks.map((block) =>
|
||||
block.id === blockId
|
||||
? { ...block, graphCoordinates: newPosition }
|
||||
: block
|
||||
)
|
||||
)
|
||||
setTypebot(
|
||||
produce(typebot, (nextTypebot) => {
|
||||
nextTypebot.blocks.byId[blockId].graphCoordinates = newPosition
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Step, StepType } from 'bot-engine'
|
||||
import { Step, StepType } from 'models'
|
||||
import {
|
||||
createContext,
|
||||
Dispatch,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Block, Step, StepType, Target } from 'bot-engine'
|
||||
import { Block, Step, Target } from 'models'
|
||||
import {
|
||||
createContext,
|
||||
Dispatch,
|
||||
@ -41,26 +41,22 @@ export type Node = Omit<Block, 'steps'> & {
|
||||
})[]
|
||||
}
|
||||
|
||||
export type NewBlockPayload = {
|
||||
x: number
|
||||
y: number
|
||||
type?: StepType
|
||||
step?: Step
|
||||
}
|
||||
|
||||
const graphPositionDefaultValue = { x: 400, y: 100, scale: 1 }
|
||||
|
||||
type ConnectingIdsProps = {
|
||||
source: { blockId: string; stepId: string }
|
||||
target?: Target
|
||||
} | null
|
||||
|
||||
type PreviewingIdsProps = { sourceId?: string; targetId?: string }
|
||||
|
||||
const graphContext = createContext<{
|
||||
graphPosition: Position
|
||||
setGraphPosition: Dispatch<SetStateAction<Position>>
|
||||
connectingIds: { blockId: string; stepId: string; target?: Target } | null
|
||||
setConnectingIds: Dispatch<
|
||||
SetStateAction<{ blockId: string; stepId: string; target?: Target } | null>
|
||||
>
|
||||
previewingIds: { sourceId?: string; targetId?: string }
|
||||
setPreviewingIds: Dispatch<
|
||||
SetStateAction<{ sourceId?: string; targetId?: string }>
|
||||
>
|
||||
connectingIds: ConnectingIdsProps
|
||||
setConnectingIds: Dispatch<SetStateAction<ConnectingIdsProps>>
|
||||
previewingIds: PreviewingIdsProps
|
||||
setPreviewingIds: Dispatch<SetStateAction<PreviewingIdsProps>>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
}>({
|
||||
@ -70,15 +66,8 @@ const graphContext = createContext<{
|
||||
|
||||
export const GraphProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [graphPosition, setGraphPosition] = useState(graphPositionDefaultValue)
|
||||
const [connectingIds, setConnectingIds] = useState<{
|
||||
blockId: string
|
||||
stepId: string
|
||||
target?: Target
|
||||
} | null>(null)
|
||||
const [previewingIds, setPreviewingIds] = useState<{
|
||||
sourceId?: string
|
||||
targetId?: string
|
||||
}>({})
|
||||
const [connectingIds, setConnectingIds] = useState<ConnectingIdsProps>(null)
|
||||
const [previewingIds, setPreviewingIds] = useState<PreviewingIdsProps>({})
|
||||
|
||||
return (
|
||||
<graphContext.Provider
|
||||
|
@ -1,402 +0,0 @@
|
||||
import { useToast } from '@chakra-ui/react'
|
||||
import {
|
||||
Block,
|
||||
PublicTypebot,
|
||||
Settings,
|
||||
Step,
|
||||
StepType,
|
||||
Target,
|
||||
Theme,
|
||||
Typebot,
|
||||
} from 'bot-engine'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { useRouter } from 'next/router'
|
||||
import {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
createPublishedTypebot,
|
||||
parseTypebotToPublicTypebot,
|
||||
updatePublishedTypebot,
|
||||
} from 'services/publicTypebot'
|
||||
import {
|
||||
checkIfPublished,
|
||||
checkIfTypebotsAreEqual,
|
||||
parseDefaultPublicId,
|
||||
parseNewBlock,
|
||||
parseNewStep,
|
||||
updateTypebot,
|
||||
} from 'services/typebots'
|
||||
import {
|
||||
fetcher,
|
||||
insertItemInList,
|
||||
omit,
|
||||
preventUserFromRefreshing,
|
||||
} from 'services/utils'
|
||||
import useSWR from 'swr'
|
||||
import { isDefined } from 'utils'
|
||||
import { NewBlockPayload, Coordinates } from './GraphContext'
|
||||
|
||||
const typebotContext = createContext<{
|
||||
typebot?: Typebot
|
||||
publishedTypebot?: PublicTypebot
|
||||
isPublished: boolean
|
||||
isPublishing: boolean
|
||||
hasUnsavedChanges: boolean
|
||||
isSavingLoading: boolean
|
||||
save: () => void
|
||||
updateStep: (
|
||||
ids: { stepId: string; blockId: string },
|
||||
updates: Partial<Step>
|
||||
) => void
|
||||
addNewBlock: (props: NewBlockPayload) => void
|
||||
updateBlockPosition: (blockId: string, newPositon: Coordinates) => void
|
||||
removeBlock: (blockId: string) => void
|
||||
addStepToBlock: (
|
||||
blockId: string,
|
||||
step: StepType | Step,
|
||||
index: number
|
||||
) => void
|
||||
removeStepFromBlock: (blockId: string, stepId: string) => void
|
||||
updateTarget: (connectingIds: {
|
||||
blockId: string
|
||||
stepId: string
|
||||
target?: Target
|
||||
}) => void
|
||||
undo: () => void
|
||||
updateTheme: (theme: Theme) => void
|
||||
updateSettings: (settings: Settings) => void
|
||||
updatePublicId: (publicId: string) => void
|
||||
publishTypebot: () => void
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
}>({})
|
||||
|
||||
export const TypebotContext = ({
|
||||
children,
|
||||
typebotId,
|
||||
}: {
|
||||
children: ReactNode
|
||||
typebotId?: string
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const toast = useToast({
|
||||
position: 'top-right',
|
||||
status: 'error',
|
||||
})
|
||||
const [undoStack, setUndoStack] = useState<Typebot[]>([])
|
||||
const { typebot, publishedTypebot, isLoading, mutate } = useFetchedTypebot({
|
||||
typebotId,
|
||||
onError: (error) =>
|
||||
toast({
|
||||
title: 'Error while fetching typebot',
|
||||
description: error.message,
|
||||
}),
|
||||
})
|
||||
const [localTypebot, setLocalTypebot] = useState<Typebot>()
|
||||
const [localPublishedTypebot, setLocalPublishedTypebot] =
|
||||
useState<PublicTypebot>()
|
||||
const [isSavingLoading, setIsSavingLoading] = useState(false)
|
||||
const [isPublishing, setIsPublishing] = useState(false)
|
||||
|
||||
const hasUnsavedChanges = useMemo(
|
||||
() =>
|
||||
isDefined(typebot) &&
|
||||
isDefined(localTypebot) &&
|
||||
!deepEqual(localTypebot, typebot),
|
||||
[typebot, localTypebot]
|
||||
)
|
||||
const isPublished = useMemo(
|
||||
() =>
|
||||
isDefined(typebot) &&
|
||||
isDefined(publishedTypebot) &&
|
||||
checkIfPublished(typebot, publishedTypebot),
|
||||
[typebot, publishedTypebot]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!localTypebot || !typebot) return
|
||||
if (!checkIfTypebotsAreEqual(localTypebot, typebot)) {
|
||||
pushNewTypebotInUndoStack(localTypebot)
|
||||
window.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||
window.addEventListener('beforeunload', preventUserFromRefreshing)
|
||||
} else {
|
||||
window.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localTypebot])
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return
|
||||
if (!typebot) {
|
||||
toast({ status: 'info', description: "Couldn't find typebot" })
|
||||
router.replace('/typebots')
|
||||
return
|
||||
}
|
||||
setLocalTypebot({ ...typebot })
|
||||
if (publishedTypebot) setLocalPublishedTypebot({ ...publishedTypebot })
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoading])
|
||||
|
||||
const pushNewTypebotInUndoStack = (typebot: Typebot) => {
|
||||
setUndoStack([...undoStack, typebot])
|
||||
}
|
||||
|
||||
const undo = () => {
|
||||
const lastTypebot = [...undoStack].pop()
|
||||
setUndoStack(undoStack.slice(0, -1))
|
||||
setLocalTypebot(lastTypebot)
|
||||
}
|
||||
|
||||
const saveTypebot = async () => {
|
||||
if (!localTypebot) return
|
||||
setIsSavingLoading(true)
|
||||
const { error } = await updateTypebot(localTypebot.id, localTypebot)
|
||||
setIsSavingLoading(false)
|
||||
if (error) return toast({ title: error.name, description: error.message })
|
||||
mutate({ typebot: localTypebot })
|
||||
window.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||
}
|
||||
|
||||
const updateBlocks = (blocks: Block[]) => {
|
||||
if (!localTypebot) return
|
||||
setLocalTypebot({
|
||||
...localTypebot,
|
||||
blocks: [...blocks],
|
||||
})
|
||||
}
|
||||
|
||||
const updateStep = (
|
||||
{ blockId, stepId }: { blockId: string; stepId: string },
|
||||
updates: Partial<Omit<Step, 'id' | 'type'>>
|
||||
) => {
|
||||
if (!localTypebot) return
|
||||
setLocalTypebot({
|
||||
...localTypebot,
|
||||
blocks: localTypebot.blocks.map((block) =>
|
||||
block.id === blockId
|
||||
? {
|
||||
...block,
|
||||
steps: block.steps.map((step) =>
|
||||
step.id === stepId ? { ...step, ...updates } : step
|
||||
),
|
||||
}
|
||||
: block
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
const addNewBlock = ({ x, y, type, step }: NewBlockPayload) => {
|
||||
if (!localTypebot) return
|
||||
updateBlocks([
|
||||
...localTypebot.blocks.filter((block) => block.steps.length > 0),
|
||||
parseNewBlock({
|
||||
step,
|
||||
type,
|
||||
totalBlocks: localTypebot.blocks.length,
|
||||
initialCoordinates: {
|
||||
x,
|
||||
y,
|
||||
},
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
const updateBlockPosition = (blockId: string, newPosition: Coordinates) => {
|
||||
if (!localTypebot) return
|
||||
blockId === 'start-block'
|
||||
? setLocalTypebot({
|
||||
...localTypebot,
|
||||
startBlock: {
|
||||
...localTypebot.startBlock,
|
||||
graphCoordinates: newPosition,
|
||||
},
|
||||
})
|
||||
: updateBlocks(
|
||||
localTypebot.blocks.map((block) =>
|
||||
block.id === blockId
|
||||
? { ...block, graphCoordinates: newPosition }
|
||||
: block
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const addStepToBlock = (
|
||||
blockId: string,
|
||||
step: StepType | Step,
|
||||
index: number
|
||||
) => {
|
||||
if (!localTypebot) return
|
||||
updateBlocks(
|
||||
localTypebot.blocks
|
||||
.map((block) =>
|
||||
block.id === blockId
|
||||
? {
|
||||
...block,
|
||||
steps: insertItemInList<Step>(
|
||||
block.steps,
|
||||
index,
|
||||
typeof step === 'string'
|
||||
? parseNewStep(step as StepType, block.id)
|
||||
: { ...step, blockId: block.id }
|
||||
),
|
||||
}
|
||||
: block
|
||||
)
|
||||
.filter((block) => block.steps.length > 0)
|
||||
)
|
||||
}
|
||||
|
||||
const removeStepFromBlock = (blockId: string, stepId: string) => {
|
||||
if (!localTypebot) return
|
||||
updateBlocks(
|
||||
localTypebot.blocks.map((block) =>
|
||||
block.id === blockId
|
||||
? {
|
||||
...block,
|
||||
steps: [...block.steps.filter((step) => step.id !== stepId)],
|
||||
}
|
||||
: block
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const updateTarget = ({
|
||||
blockId,
|
||||
stepId,
|
||||
target,
|
||||
}: {
|
||||
blockId: string
|
||||
stepId: string
|
||||
target?: Target
|
||||
}) => {
|
||||
if (!localTypebot) return
|
||||
blockId === 'start-block'
|
||||
? setLocalTypebot({
|
||||
...localTypebot,
|
||||
startBlock: {
|
||||
...localTypebot.startBlock,
|
||||
steps: [{ ...localTypebot.startBlock.steps[0], target }],
|
||||
},
|
||||
})
|
||||
: updateBlocks(
|
||||
localTypebot.blocks.map((block) =>
|
||||
block.id === blockId
|
||||
? {
|
||||
...block,
|
||||
steps: [
|
||||
...block.steps.map((step) =>
|
||||
step.id === stepId ? { ...step, target } : step
|
||||
),
|
||||
],
|
||||
}
|
||||
: block
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const removeBlock = (blockId: string) => {
|
||||
if (!localTypebot) return
|
||||
const blocks = [...localTypebot.blocks.filter((b) => b.id !== blockId)]
|
||||
setLocalTypebot({ ...localTypebot, blocks })
|
||||
}
|
||||
|
||||
const updateTheme = (theme: Theme) => {
|
||||
if (!localTypebot) return
|
||||
setLocalTypebot({ ...localTypebot, theme })
|
||||
}
|
||||
|
||||
const updateSettings = (settings: Settings) => {
|
||||
if (!localTypebot) return
|
||||
setLocalTypebot({ ...localTypebot, settings })
|
||||
}
|
||||
|
||||
const updatePublicId = (publicId: string) => {
|
||||
if (!localTypebot) return
|
||||
setLocalTypebot({ ...localTypebot, publicId })
|
||||
}
|
||||
|
||||
const publishTypebot = async () => {
|
||||
if (!localTypebot) return
|
||||
if (!localPublishedTypebot) {
|
||||
const newPublicId = parseDefaultPublicId(
|
||||
localTypebot.name,
|
||||
localTypebot.id
|
||||
)
|
||||
updatePublicId(newPublicId)
|
||||
localTypebot.publicId = newPublicId
|
||||
}
|
||||
if (hasUnsavedChanges || !localPublishedTypebot) await saveTypebot()
|
||||
setIsPublishing(true)
|
||||
if (localPublishedTypebot) {
|
||||
const { error } = await updatePublishedTypebot(
|
||||
localPublishedTypebot.id,
|
||||
omit(parseTypebotToPublicTypebot(localTypebot), 'id')
|
||||
)
|
||||
setIsPublishing(false)
|
||||
if (error) return toast({ title: error.name, description: error.message })
|
||||
} else {
|
||||
const { error } = await createPublishedTypebot(
|
||||
omit(parseTypebotToPublicTypebot(localTypebot), 'id')
|
||||
)
|
||||
setIsPublishing(false)
|
||||
if (error) return toast({ title: error.name, description: error.message })
|
||||
}
|
||||
mutate({ typebot: localTypebot })
|
||||
}
|
||||
|
||||
return (
|
||||
<typebotContext.Provider
|
||||
value={{
|
||||
typebot: localTypebot,
|
||||
publishedTypebot: localPublishedTypebot,
|
||||
updateStep,
|
||||
addNewBlock,
|
||||
addStepToBlock,
|
||||
updateTarget,
|
||||
removeStepFromBlock,
|
||||
updateBlockPosition,
|
||||
hasUnsavedChanges,
|
||||
isSavingLoading,
|
||||
save: saveTypebot,
|
||||
removeBlock,
|
||||
undo,
|
||||
updateTheme,
|
||||
updateSettings,
|
||||
updatePublicId,
|
||||
publishTypebot,
|
||||
isPublishing,
|
||||
isPublished,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</typebotContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useTypebot = () => useContext(typebotContext)
|
||||
|
||||
export const useFetchedTypebot = ({
|
||||
typebotId,
|
||||
onError,
|
||||
}: {
|
||||
typebotId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<
|
||||
{ typebot: Typebot; publishedTypebot?: PublicTypebot },
|
||||
Error
|
||||
>(typebotId ? `/api/typebots/${typebotId}` : null, fetcher)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
typebot: data?.typebot,
|
||||
publishedTypebot: data?.publishedTypebot,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
232
apps/builder/contexts/TypebotContext/TypebotContext.tsx
Normal file
232
apps/builder/contexts/TypebotContext/TypebotContext.tsx
Normal file
@ -0,0 +1,232 @@
|
||||
import { useToast } from '@chakra-ui/react'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { PublicTypebot, Settings, Theme, Typebot } from 'models'
|
||||
import { useRouter } from 'next/router'
|
||||
import {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
createPublishedTypebot,
|
||||
parseTypebotToPublicTypebot,
|
||||
updatePublishedTypebot,
|
||||
} from 'services/publicTypebot'
|
||||
import {
|
||||
checkIfPublished,
|
||||
checkIfTypebotsAreEqual,
|
||||
parseDefaultPublicId,
|
||||
updateTypebot,
|
||||
} from 'services/typebots'
|
||||
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'
|
||||
|
||||
type UpdateTypebotPayload = Partial<{
|
||||
theme: Theme
|
||||
settings: Settings
|
||||
publicId: string
|
||||
}>
|
||||
const typebotContext = createContext<
|
||||
{
|
||||
typebot?: Typebot
|
||||
publishedTypebot?: PublicTypebot
|
||||
isPublished: boolean
|
||||
isPublishing: boolean
|
||||
hasUnsavedChanges: boolean
|
||||
isSavingLoading: boolean
|
||||
save: () => void
|
||||
undo: () => void
|
||||
updateTypebot: (updates: UpdateTypebotPayload) => void
|
||||
publishTypebot: () => void
|
||||
} & BlocksActions &
|
||||
StepsActions
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
>({})
|
||||
|
||||
export const TypebotContext = ({
|
||||
children,
|
||||
typebotId,
|
||||
}: {
|
||||
children: ReactNode
|
||||
typebotId?: string
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const toast = useToast({
|
||||
position: 'top-right',
|
||||
status: 'error',
|
||||
})
|
||||
const [undoStack, setUndoStack] = useState<Typebot[]>([])
|
||||
const { typebot, publishedTypebot, isLoading, mutate } = useFetchedTypebot({
|
||||
typebotId,
|
||||
onError: (error) =>
|
||||
toast({
|
||||
title: 'Error while fetching typebot',
|
||||
description: error.message,
|
||||
}),
|
||||
})
|
||||
const [localTypebot, setLocalTypebot] = useImmer<Typebot | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [localPublishedTypebot, setLocalPublishedTypebot] =
|
||||
useState<PublicTypebot>()
|
||||
const [isSavingLoading, setIsSavingLoading] = useState(false)
|
||||
const [isPublishing, setIsPublishing] = useState(false)
|
||||
|
||||
const hasUnsavedChanges = useMemo(
|
||||
() =>
|
||||
isDefined(typebot) &&
|
||||
isDefined(localTypebot) &&
|
||||
!deepEqual(localTypebot, typebot),
|
||||
[typebot, localTypebot]
|
||||
)
|
||||
const isPublished = useMemo(
|
||||
() =>
|
||||
isDefined(typebot) &&
|
||||
isDefined(publishedTypebot) &&
|
||||
checkIfPublished(typebot, publishedTypebot),
|
||||
[typebot, publishedTypebot]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!localTypebot || !typebot) return
|
||||
if (!checkIfTypebotsAreEqual(localTypebot, typebot)) {
|
||||
pushNewTypebotInUndoStack(localTypebot)
|
||||
window.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||
window.addEventListener('beforeunload', preventUserFromRefreshing)
|
||||
} else {
|
||||
window.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localTypebot])
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return
|
||||
if (!typebot) {
|
||||
toast({ status: 'info', description: "Couldn't find typebot" })
|
||||
router.replace('/typebots')
|
||||
return
|
||||
}
|
||||
setLocalTypebot({ ...typebot })
|
||||
if (publishedTypebot) setLocalPublishedTypebot({ ...publishedTypebot })
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoading])
|
||||
|
||||
const pushNewTypebotInUndoStack = (typebot: Typebot) => {
|
||||
setUndoStack([...undoStack, typebot])
|
||||
}
|
||||
|
||||
const undo = () => {
|
||||
const lastTypebot = [...undoStack].pop()
|
||||
setUndoStack(undoStack.slice(0, -1))
|
||||
setLocalTypebot(lastTypebot)
|
||||
}
|
||||
|
||||
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,
|
||||
}: UpdateTypebotPayload) => {
|
||||
setLocalTypebot((typebot) => {
|
||||
if (!typebot) return
|
||||
if (publicId) typebot.publicId = publicId
|
||||
if (settings) typebot.settings = settings
|
||||
if (theme) typebot.theme = theme
|
||||
})
|
||||
}
|
||||
|
||||
const publishTypebot = async () => {
|
||||
if (!localTypebot) return
|
||||
const newLocalTypebot = { ...localTypebot }
|
||||
if (!localPublishedTypebot) {
|
||||
const newPublicId = parseDefaultPublicId(
|
||||
localTypebot.name,
|
||||
localTypebot.id
|
||||
)
|
||||
updateLocalTypebot({ publicId: newPublicId })
|
||||
newLocalTypebot.publicId = newPublicId
|
||||
}
|
||||
if (hasUnsavedChanges || !localPublishedTypebot)
|
||||
await saveTypebot(newLocalTypebot)
|
||||
setIsPublishing(true)
|
||||
if (localPublishedTypebot) {
|
||||
const { error } = await updatePublishedTypebot(
|
||||
localPublishedTypebot.id,
|
||||
omit(parseTypebotToPublicTypebot(newLocalTypebot), 'id')
|
||||
)
|
||||
setIsPublishing(false)
|
||||
if (error) return toast({ title: error.name, description: error.message })
|
||||
} else {
|
||||
const { data, error } = await createPublishedTypebot(
|
||||
omit(parseTypebotToPublicTypebot(newLocalTypebot), 'id')
|
||||
)
|
||||
setLocalPublishedTypebot(data)
|
||||
setIsPublishing(false)
|
||||
if (error) return toast({ title: error.name, description: error.message })
|
||||
}
|
||||
mutate({ typebot: localTypebot })
|
||||
}
|
||||
|
||||
return (
|
||||
<typebotContext.Provider
|
||||
value={{
|
||||
typebot: localTypebot,
|
||||
publishedTypebot: localPublishedTypebot,
|
||||
hasUnsavedChanges,
|
||||
isSavingLoading,
|
||||
save: saveTypebot,
|
||||
undo,
|
||||
publishTypebot,
|
||||
isPublishing,
|
||||
isPublished,
|
||||
updateTypebot: updateLocalTypebot,
|
||||
...blocksActions(setLocalTypebot as Updater<Typebot>),
|
||||
...stepsAction(setLocalTypebot as Updater<Typebot>),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</typebotContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useTypebot = () => useContext(typebotContext)
|
||||
|
||||
export const useFetchedTypebot = ({
|
||||
typebotId,
|
||||
onError,
|
||||
}: {
|
||||
typebotId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<
|
||||
{ typebot: Typebot; publishedTypebot?: PublicTypebot },
|
||||
Error
|
||||
>(typebotId ? `/api/typebots/${typebotId}` : null, fetcher)
|
||||
if (error) onError(error)
|
||||
return {
|
||||
typebot: data?.typebot,
|
||||
publishedTypebot: data?.publishedTypebot,
|
||||
isLoading: !error && !data,
|
||||
mutate,
|
||||
}
|
||||
}
|
40
apps/builder/contexts/TypebotContext/actions/blocks.ts
Normal file
40
apps/builder/contexts/TypebotContext/actions/blocks.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { Coordinates } from 'contexts/GraphContext'
|
||||
import { Block, Step, StepType, Typebot } from 'models'
|
||||
import { parseNewBlock } from 'services/typebots'
|
||||
import { Updater } from 'use-immer'
|
||||
import { createStepDraft, deleteStepDraft } from './steps'
|
||||
|
||||
export type BlocksActions = {
|
||||
createBlock: (props: Coordinates & { step: StepType | Step }) => void
|
||||
updateBlock: (blockId: string, updates: Partial<Omit<Block, 'id'>>) => void
|
||||
deleteBlock: (blockId: string) => void
|
||||
}
|
||||
|
||||
export const blocksActions = (setTypebot: Updater<Typebot>): BlocksActions => ({
|
||||
createBlock: ({ x, y, step }: Coordinates & { step: StepType | Step }) => {
|
||||
setTypebot((typebot) => {
|
||||
const newBlock = parseNewBlock({
|
||||
totalBlocks: typebot.blocks.allIds.length,
|
||||
initialCoordinates: { x, y },
|
||||
})
|
||||
typebot.blocks.byId[newBlock.id] = newBlock
|
||||
typebot.blocks.allIds.push(newBlock.id)
|
||||
createStepDraft(typebot, step, newBlock.id)
|
||||
})
|
||||
},
|
||||
updateBlock: (blockId: string, updates: Partial<Omit<Block, 'id'>>) =>
|
||||
setTypebot((typebot) => {
|
||||
typebot.blocks.byId[blockId] = {
|
||||
...typebot.blocks.byId[blockId],
|
||||
...updates,
|
||||
}
|
||||
}),
|
||||
deleteBlock: (blockId: string) =>
|
||||
setTypebot((typebot) => {
|
||||
const block = typebot.blocks.byId[blockId]
|
||||
block.stepIds.forEach((stepId) => deleteStepDraft(typebot, stepId))
|
||||
delete typebot.blocks.byId[blockId]
|
||||
const index = typebot.blocks.allIds.indexOf(blockId)
|
||||
if (index !== -1) typebot.blocks.allIds.splice(index, 1)
|
||||
}),
|
||||
})
|
51
apps/builder/contexts/TypebotContext/actions/steps.ts
Normal file
51
apps/builder/contexts/TypebotContext/actions/steps.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { Step, StepType, Typebot } from 'models'
|
||||
import { parseNewStep } from 'services/typebots'
|
||||
import { Updater } from 'use-immer'
|
||||
import { WritableDraft } from 'immer/dist/types/types-external'
|
||||
|
||||
export type StepsActions = {
|
||||
createStep: (blockId: string, step: StepType | Step, index?: number) => void
|
||||
updateStep: (
|
||||
stepId: string,
|
||||
updates: Partial<Omit<Step, 'id' | 'type'>>
|
||||
) => void
|
||||
deleteStep: (stepId: string) => void
|
||||
}
|
||||
|
||||
export const stepsAction = (setTypebot: Updater<Typebot>): StepsActions => ({
|
||||
createStep: (blockId: string, step: StepType | Step, index?: number) => {
|
||||
setTypebot((typebot) => {
|
||||
createStepDraft(typebot, step, blockId, index)
|
||||
})
|
||||
},
|
||||
updateStep: (stepId: string, updates: Partial<Omit<Step, 'id' | 'type'>>) =>
|
||||
setTypebot((typebot) => {
|
||||
typebot.steps.byId[stepId] = { ...typebot.steps.byId[stepId], ...updates }
|
||||
}),
|
||||
deleteStep: (stepId: string) => {
|
||||
setTypebot((typebot) => {
|
||||
deleteStepDraft(typebot, stepId)
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
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 createStepDraft = (
|
||||
typebot: WritableDraft<Typebot>,
|
||||
step: StepType | Step,
|
||||
blockId: string,
|
||||
index?: number
|
||||
) => {
|
||||
const newStep = typeof step === 'string' ? parseNewStep(step, blockId) : step
|
||||
typebot.steps.byId[newStep.id] = newStep
|
||||
typebot.steps.allIds.push(newStep.id)
|
||||
typebot.blocks.byId[blockId].stepIds.splice(index ?? 0, 0, newStep.id)
|
||||
}
|
1
apps/builder/contexts/TypebotContext/index.ts
Normal file
1
apps/builder/contexts/TypebotContext/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { TypebotContext } from './TypebotContext'
|
Reference in New Issue
Block a user