🚀 Init preview and typebot cotext in editor
This commit is contained in:
35
apps/builder/contexts/EditorContext.tsx
Normal file
35
apps/builder/contexts/EditorContext.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import {
|
||||
createContext,
|
||||
Dispatch,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
useContext,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
export enum RightPanel {
|
||||
PREVIEW,
|
||||
}
|
||||
const editorContext = createContext<{
|
||||
rightPanel?: RightPanel
|
||||
setRightPanel: Dispatch<SetStateAction<RightPanel | undefined>>
|
||||
}>({
|
||||
setRightPanel: () => console.log("I'm not instantiated"),
|
||||
})
|
||||
|
||||
export const EditorContext = ({ children }: { children: ReactNode }) => {
|
||||
const [rightPanel, setRightPanel] = useState<RightPanel>()
|
||||
|
||||
return (
|
||||
<editorContext.Provider
|
||||
value={{
|
||||
rightPanel,
|
||||
setRightPanel,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</editorContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useEditor = () => useContext(editorContext)
|
@ -1,4 +1,4 @@
|
||||
import { Block, StartBlock, Step, StepType, Target } from 'bot-engine'
|
||||
import { Block, Step, StepType, Target } from 'bot-engine'
|
||||
import {
|
||||
createContext,
|
||||
Dispatch,
|
||||
@ -7,8 +7,6 @@ import {
|
||||
useContext,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { parseNewBlock, parseNewStep } from 'services/graph'
|
||||
import { insertItemInList } from 'services/utils'
|
||||
|
||||
export const stubLength = 20
|
||||
export const blockWidth = 300
|
||||
@ -27,7 +25,7 @@ export const blockAnchorsOffset = {
|
||||
},
|
||||
}
|
||||
export const firstStepOffsetY = 88
|
||||
export const spaceBetweenSteps = 66
|
||||
export const spaceBetweenSteps = 62
|
||||
|
||||
export type Coordinates = { x: number; y: number }
|
||||
|
||||
@ -59,38 +57,15 @@ const graphContext = createContext<{
|
||||
setConnectingIds: Dispatch<
|
||||
SetStateAction<{ blockId: string; stepId: string; target?: Target } | null>
|
||||
>
|
||||
startBlock?: StartBlock
|
||||
setStartBlock: Dispatch<SetStateAction<StartBlock | undefined>>
|
||||
blocks: Block[]
|
||||
setBlocks: Dispatch<SetStateAction<Block[]>>
|
||||
addNewBlock: (props: NewBlockPayload) => void
|
||||
updateBlockPosition: (blockId: string, newPositon: Coordinates) => void
|
||||
addNewStepToBlock: (
|
||||
blockId: string,
|
||||
step: StepType | Step,
|
||||
index: number
|
||||
) => void
|
||||
removeStepFromBlock: (blockId: string, stepId: string) => void
|
||||
addTarget: (connectingIds: {
|
||||
blockId: string
|
||||
stepId: string
|
||||
target?: Target
|
||||
}) => void
|
||||
removeTarget: (connectingIds: { blockId: string; stepId: string }) => void
|
||||
previewingIds: { sourceId?: string; targetId?: string }
|
||||
setPreviewingIds: Dispatch<
|
||||
SetStateAction<{ sourceId?: string; targetId?: string }>
|
||||
>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
}>({
|
||||
graphPosition: graphPositionDefaultValue,
|
||||
setGraphPosition: () => console.log("I'm not instantiated"),
|
||||
connectingIds: null,
|
||||
setConnectingIds: () => console.log("I'm not instantiated"),
|
||||
blocks: [],
|
||||
setBlocks: () => console.log("I'm not instantiated"),
|
||||
updateBlockPosition: () => console.log("I'm not instantiated"),
|
||||
addNewStepToBlock: () => console.log("I'm not instantiated"),
|
||||
addNewBlock: () => console.log("I'm not instantiated"),
|
||||
removeStepFromBlock: () => console.log("I'm not instantiated"),
|
||||
addTarget: () => console.log("I'm not instantiated"),
|
||||
removeTarget: () => console.log("I'm not instantiated"),
|
||||
setStartBlock: () => console.log("I'm not instantiated"),
|
||||
})
|
||||
|
||||
export const GraphProvider = ({ children }: { children: ReactNode }) => {
|
||||
@ -100,125 +75,10 @@ export const GraphProvider = ({ children }: { children: ReactNode }) => {
|
||||
stepId: string
|
||||
target?: Target
|
||||
} | null>(null)
|
||||
const [blocks, setBlocks] = useState<Block[]>([])
|
||||
const [startBlock, setStartBlock] = useState<StartBlock | undefined>()
|
||||
|
||||
const addNewBlock = ({ x, y, type, step }: NewBlockPayload) => {
|
||||
const boardCoordinates = {
|
||||
x,
|
||||
y,
|
||||
}
|
||||
setBlocks((blocks) => [
|
||||
...blocks.filter((block) => block.steps.length > 0),
|
||||
parseNewBlock({
|
||||
step,
|
||||
type,
|
||||
totalBlocks: blocks.length,
|
||||
initialCoordinates: boardCoordinates,
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
const updateBlockPosition = (blockId: string, newPosition: Coordinates) => {
|
||||
setBlocks((blocks) =>
|
||||
blocks.map((block) =>
|
||||
block.id === blockId
|
||||
? { ...block, graphCoordinates: newPosition }
|
||||
: block
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const addNewStepToBlock = (
|
||||
blockId: string,
|
||||
step: StepType | Step,
|
||||
index: number
|
||||
) => {
|
||||
setBlocks((blocks) =>
|
||||
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) => {
|
||||
setBlocks((blocks) =>
|
||||
blocks.map((block) =>
|
||||
block.id === blockId
|
||||
? {
|
||||
...block,
|
||||
steps: [...block.steps.filter((step) => step.id !== stepId)],
|
||||
}
|
||||
: block
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const addTarget = ({
|
||||
blockId,
|
||||
stepId,
|
||||
target,
|
||||
}: {
|
||||
blockId: string
|
||||
stepId: string
|
||||
target?: Target
|
||||
}) => {
|
||||
startBlock && blockId === 'start-block'
|
||||
? setStartBlock({
|
||||
...startBlock,
|
||||
steps: [{ ...startBlock.steps[0], target }],
|
||||
})
|
||||
: setBlocks((blocks) =>
|
||||
blocks.map((block) =>
|
||||
block.id === blockId
|
||||
? {
|
||||
...block,
|
||||
steps: [
|
||||
...block.steps.map((step) =>
|
||||
step.id === stepId ? { ...step, target } : step
|
||||
),
|
||||
],
|
||||
}
|
||||
: block
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const removeTarget = ({
|
||||
blockId,
|
||||
stepId,
|
||||
}: {
|
||||
blockId: string
|
||||
stepId: string
|
||||
}) => {
|
||||
setBlocks((blocks) =>
|
||||
blocks.map((block) =>
|
||||
block.id === blockId
|
||||
? {
|
||||
...block,
|
||||
steps: [
|
||||
...block.steps.map((step) =>
|
||||
step.id === stepId ? { ...step, target: undefined } : step
|
||||
),
|
||||
],
|
||||
}
|
||||
: block
|
||||
)
|
||||
)
|
||||
}
|
||||
const [previewingIds, setPreviewingIds] = useState<{
|
||||
sourceId?: string
|
||||
targetId?: string
|
||||
}>({})
|
||||
|
||||
return (
|
||||
<graphContext.Provider
|
||||
@ -227,16 +87,8 @@ export const GraphProvider = ({ children }: { children: ReactNode }) => {
|
||||
setGraphPosition,
|
||||
connectingIds,
|
||||
setConnectingIds,
|
||||
blocks,
|
||||
setBlocks,
|
||||
updateBlockPosition,
|
||||
addNewStepToBlock,
|
||||
addNewBlock,
|
||||
removeStepFromBlock,
|
||||
addTarget,
|
||||
removeTarget,
|
||||
startBlock,
|
||||
setStartBlock,
|
||||
previewingIds,
|
||||
setPreviewingIds,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -1,12 +1,53 @@
|
||||
import { useToast } from '@chakra-ui/react'
|
||||
import { Typebot } from 'bot-engine'
|
||||
import { Block, Step, StepType, Target, Typebot } from 'bot-engine'
|
||||
import { useRouter } from 'next/router'
|
||||
import { createContext, ReactNode, useContext, useEffect } from 'react'
|
||||
import { fetcher } from 'services/utils'
|
||||
import {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
checkIfTypebotsAreEqual,
|
||||
parseNewBlock,
|
||||
parseNewStep,
|
||||
updateTypebot,
|
||||
} from 'services/typebots'
|
||||
import {
|
||||
fetcher,
|
||||
insertItemInList,
|
||||
preventUserFromRefreshing,
|
||||
} from 'services/utils'
|
||||
import useSWR from 'swr'
|
||||
import { NewBlockPayload, Coordinates } from './GraphContext'
|
||||
|
||||
const typebotContext = createContext<{
|
||||
typebot?: Typebot
|
||||
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
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
}>({})
|
||||
|
||||
export const TypebotContext = ({
|
||||
@ -21,7 +62,8 @@ export const TypebotContext = ({
|
||||
position: 'top-right',
|
||||
status: 'error',
|
||||
})
|
||||
const { typebot, isLoading } = useFetchedTypebot({
|
||||
const [undoStack, setUndoStack] = useState<Typebot[]>([])
|
||||
const { typebot, isLoading, mutate } = useFetchedTypebot({
|
||||
typebotId,
|
||||
onError: (error) =>
|
||||
toast({
|
||||
@ -29,20 +71,214 @@ export const TypebotContext = ({
|
||||
description: error.message,
|
||||
}),
|
||||
})
|
||||
const [localTypebot, setLocalTypebot] = useState<Typebot>()
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
|
||||
const [isSavingLoading, setIsSavingLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!localTypebot || !typebot) return
|
||||
if (!checkIfTypebotsAreEqual(localTypebot, typebot)) {
|
||||
setHasUnsavedChanges(true)
|
||||
pushNewTypebotInUndoStack(localTypebot)
|
||||
window.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||
window.addEventListener('beforeunload', preventUserFromRefreshing)
|
||||
} else {
|
||||
setHasUnsavedChanges(false)
|
||||
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 })
|
||||
// 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 })
|
||||
setHasUnsavedChanges(false)
|
||||
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 })
|
||||
}
|
||||
|
||||
return (
|
||||
<typebotContext.Provider
|
||||
value={{
|
||||
typebot,
|
||||
typebot: localTypebot,
|
||||
updateStep,
|
||||
addNewBlock,
|
||||
addStepToBlock,
|
||||
updateTarget,
|
||||
removeStepFromBlock,
|
||||
updateBlockPosition,
|
||||
hasUnsavedChanges,
|
||||
isSavingLoading,
|
||||
save: saveTypebot,
|
||||
removeBlock,
|
||||
undo,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
Reference in New Issue
Block a user