2
0

🚀 Init preview and typebot cotext in editor

This commit is contained in:
Baptiste Arnaud
2021-12-22 14:59:07 +01:00
parent a54e42f255
commit b7cdc0d14a
87 changed files with 4431 additions and 735 deletions

View 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)

View File

@ -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}

View File

@ -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}