🦴 Add viewer backbone
This commit is contained in:
@@ -14,7 +14,7 @@ import { useEditor } from 'contexts/EditorContext'
|
||||
import { useGraph } from 'contexts/GraphContext'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { parseTypebotToPublicTypebot } from 'services/typebots'
|
||||
import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
|
||||
|
||||
export const PreviewDrawer = () => {
|
||||
const { typebot } = useTypebot()
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { Button } from '@chakra-ui/react'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
|
||||
export const PublishButton = () => {
|
||||
const { isPublishing, isPublished, publishTypebot } = useTypebot()
|
||||
|
||||
return (
|
||||
<Button ml={2} colorScheme={'blue'}>
|
||||
Publish
|
||||
<Button
|
||||
ml={2}
|
||||
colorScheme="blue"
|
||||
isLoading={isPublishing}
|
||||
isDisabled={isPublished}
|
||||
onClick={publishTypebot}
|
||||
>
|
||||
{isPublished ? 'Published' : 'Publish'}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Flex } from '@chakra-ui/react'
|
||||
import { TypebotViewer } from 'bot-engine'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import React, { useMemo } from 'react'
|
||||
import { parseTypebotToPublicTypebot } from 'services/typebots'
|
||||
import { parseTypebotToPublicTypebot } from 'services/publicTypebot'
|
||||
import { SideMenu } from './SideMenu'
|
||||
|
||||
export const ThemeContent = () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useToast } from '@chakra-ui/react'
|
||||
import {
|
||||
Block,
|
||||
PublicTypebot,
|
||||
Settings,
|
||||
Step,
|
||||
StepType,
|
||||
@@ -8,16 +9,25 @@ import {
|
||||
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,
|
||||
@@ -25,6 +35,8 @@ import {
|
||||
import {
|
||||
fetcher,
|
||||
insertItemInList,
|
||||
isDefined,
|
||||
omit,
|
||||
preventUserFromRefreshing,
|
||||
} from 'services/utils'
|
||||
import useSWR from 'swr'
|
||||
@@ -32,6 +44,9 @@ import { NewBlockPayload, Coordinates } from './GraphContext'
|
||||
|
||||
const typebotContext = createContext<{
|
||||
typebot?: Typebot
|
||||
publishedTypebot?: PublicTypebot
|
||||
isPublished: boolean
|
||||
isPublishing: boolean
|
||||
hasUnsavedChanges: boolean
|
||||
isSavingLoading: boolean
|
||||
save: () => void
|
||||
@@ -57,6 +72,7 @@ const typebotContext = createContext<{
|
||||
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
|
||||
}>({})
|
||||
@@ -74,7 +90,7 @@ export const TypebotContext = ({
|
||||
status: 'error',
|
||||
})
|
||||
const [undoStack, setUndoStack] = useState<Typebot[]>([])
|
||||
const { typebot, isLoading, mutate } = useFetchedTypebot({
|
||||
const { typebot, publishedTypebot, isLoading, mutate } = useFetchedTypebot({
|
||||
typebotId,
|
||||
onError: (error) =>
|
||||
toast({
|
||||
@@ -83,18 +99,33 @@ export const TypebotContext = ({
|
||||
}),
|
||||
})
|
||||
const [localTypebot, setLocalTypebot] = useState<Typebot>()
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
|
||||
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)) {
|
||||
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
|
||||
@@ -108,6 +139,7 @@ export const TypebotContext = ({
|
||||
return
|
||||
}
|
||||
setLocalTypebot({ ...typebot })
|
||||
if (publishedTypebot) setLocalPublishedTypebot({ ...publishedTypebot })
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoading])
|
||||
|
||||
@@ -128,7 +160,6 @@ export const TypebotContext = ({
|
||||
setIsSavingLoading(false)
|
||||
if (error) return toast({ title: error.name, description: error.message })
|
||||
mutate({ typebot: localTypebot })
|
||||
setHasUnsavedChanges(false)
|
||||
window.removeEventListener('beforeunload', preventUserFromRefreshing)
|
||||
}
|
||||
|
||||
@@ -290,10 +321,34 @@ export const TypebotContext = ({
|
||||
setLocalTypebot({ ...localTypebot, publicId })
|
||||
}
|
||||
|
||||
const publishTypebot = async () => {
|
||||
if (!localTypebot) return
|
||||
if (!localPublishedTypebot)
|
||||
updatePublicId(parseDefaultPublicId(localTypebot.name, localTypebot.id))
|
||||
if (hasUnsavedChanges) 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,
|
||||
@@ -308,6 +363,9 @@ export const TypebotContext = ({
|
||||
updateTheme,
|
||||
updateSettings,
|
||||
updatePublicId,
|
||||
publishTypebot,
|
||||
isPublishing,
|
||||
isPublished,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -324,13 +382,14 @@ export const useFetchedTypebot = ({
|
||||
typebotId?: string
|
||||
onError: (error: Error) => void
|
||||
}) => {
|
||||
const { data, error, mutate } = useSWR<{ typebot: Typebot }, Error>(
|
||||
typebotId ? `/api/typebots/${typebotId}` : null,
|
||||
fetcher
|
||||
)
|
||||
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,
|
||||
}
|
||||
|
||||
30
apps/builder/pages/api/publicTypebots.ts
Normal file
30
apps/builder/pages/api/publicTypebots.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import prisma from 'libs/prisma'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { getSession } from 'next-auth/react'
|
||||
import { methodNotAllowed } from 'services/api/utils'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const session = await getSession({ req })
|
||||
|
||||
if (!session?.user)
|
||||
return res.status(401).json({ message: 'Not authenticated' })
|
||||
|
||||
try {
|
||||
if (req.method === 'POST') {
|
||||
const data = JSON.parse(req.body)
|
||||
const typebot = await prisma.publicTypebot.create({
|
||||
data: { ...data },
|
||||
})
|
||||
return res.send(typebot)
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
if (err instanceof Error) {
|
||||
return res.status(500).send({ title: err.name, message: err.message })
|
||||
}
|
||||
return res.status(500).send({ message: 'An error occured', error: err })
|
||||
}
|
||||
}
|
||||
|
||||
export default handler
|
||||
24
apps/builder/pages/api/publicTypebots/[id].ts
Normal file
24
apps/builder/pages/api/publicTypebots/[id].ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import prisma from 'libs/prisma'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import { getSession } from 'next-auth/react'
|
||||
import { methodNotAllowed } from 'services/api/utils'
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const session = await getSession({ req })
|
||||
|
||||
if (!session?.user)
|
||||
return res.status(401).json({ message: 'Not authenticated' })
|
||||
|
||||
const id = req.query.id.toString()
|
||||
if (req.method === 'PUT') {
|
||||
const data = JSON.parse(req.body)
|
||||
const typebots = await prisma.publicTypebot.update({
|
||||
where: { id },
|
||||
data,
|
||||
})
|
||||
return res.send({ typebots })
|
||||
}
|
||||
return methodNotAllowed(res)
|
||||
}
|
||||
|
||||
export default handler
|
||||
@@ -13,8 +13,13 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === 'GET') {
|
||||
const typebot = await prisma.typebot.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
publishedTypebot: true,
|
||||
},
|
||||
})
|
||||
return res.send({ typebot })
|
||||
if (!typebot) return res.send({ typebot: null })
|
||||
const { publishedTypebot, ...restOfTypebot } = typebot
|
||||
return res.send({ typebot: restOfTypebot, publishedTypebot })
|
||||
}
|
||||
if (req.method === 'DELETE') {
|
||||
const typebots = await prisma.typebot.delete({
|
||||
|
||||
35
apps/builder/services/publicTypebot.ts
Normal file
35
apps/builder/services/publicTypebot.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { PublicTypebot, Typebot } from 'bot-engine'
|
||||
import { sendRequest } from './utils'
|
||||
import shortId from 'short-uuid'
|
||||
|
||||
export const parseTypebotToPublicTypebot = (
|
||||
typebot: Typebot
|
||||
): PublicTypebot => ({
|
||||
id: shortId.generate(),
|
||||
blocks: typebot.blocks,
|
||||
name: typebot.name,
|
||||
startBlock: typebot.startBlock,
|
||||
typebotId: typebot.id,
|
||||
theme: typebot.theme,
|
||||
settings: typebot.settings,
|
||||
publicId: typebot.publicId,
|
||||
})
|
||||
|
||||
export const createPublishedTypebot = async (
|
||||
typebot: Omit<PublicTypebot, 'id'>
|
||||
) =>
|
||||
sendRequest({
|
||||
url: `/api/publicTypebots`,
|
||||
method: 'POST',
|
||||
body: typebot,
|
||||
})
|
||||
|
||||
export const updatePublishedTypebot = async (
|
||||
id: string,
|
||||
typebot: Omit<PublicTypebot, 'id'>
|
||||
) =>
|
||||
sendRequest({
|
||||
url: `/api/publicTypebots/${id}`,
|
||||
method: 'PUT',
|
||||
body: typebot,
|
||||
})
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
StepType,
|
||||
Block,
|
||||
TextStep,
|
||||
PublicTypebot,
|
||||
TextInputStep,
|
||||
PublicTypebot,
|
||||
} from 'bot-engine'
|
||||
import shortId from 'short-uuid'
|
||||
import { Typebot } from 'bot-engine'
|
||||
@@ -154,18 +154,16 @@ export const checkIfTypebotsAreEqual = (
|
||||
}
|
||||
)
|
||||
|
||||
export const parseTypebotToPublicTypebot = (
|
||||
typebot: Typebot
|
||||
): PublicTypebot => ({
|
||||
id: shortId.generate(),
|
||||
blocks: typebot.blocks,
|
||||
name: typebot.name,
|
||||
startBlock: typebot.startBlock,
|
||||
typebotId: typebot.id,
|
||||
theme: typebot.theme,
|
||||
settings: typebot.settings,
|
||||
publicId: typebot.publicId,
|
||||
})
|
||||
export const checkIfPublished = (
|
||||
typebot: Typebot,
|
||||
publicTypebot: PublicTypebot
|
||||
) =>
|
||||
deepEqual(typebot.blocks, publicTypebot.blocks) &&
|
||||
deepEqual(typebot.startBlock, publicTypebot.startBlock) &&
|
||||
typebot.name === publicTypebot.name &&
|
||||
typebot.publicId === publicTypebot.publicId &&
|
||||
deepEqual(typebot.settings, publicTypebot.settings) &&
|
||||
deepEqual(typebot.theme, publicTypebot.theme)
|
||||
|
||||
export const parseDefaultPublicId = (name: string, id: string) =>
|
||||
toKebabCase(`${name}-${id?.slice(0, 5)}`)
|
||||
|
||||
@@ -71,3 +71,23 @@ export const toKebabCase = (value: string) => {
|
||||
if (!matched) return ''
|
||||
return matched.map((x) => x.toLowerCase()).join('-')
|
||||
}
|
||||
|
||||
interface Omit {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
<T extends object, K extends [...(keyof T)[]]>(obj: T, ...keys: K): {
|
||||
[K2 in Exclude<keyof T, K[number]>]: T[K2]
|
||||
}
|
||||
}
|
||||
|
||||
export const omit: Omit = (obj, ...keys) => {
|
||||
const ret = {} as {
|
||||
[K in keyof typeof obj]: typeof obj[K]
|
||||
}
|
||||
let key: keyof typeof obj
|
||||
for (key in obj) {
|
||||
if (!keys.includes(key)) {
|
||||
ret[key] = obj[key]
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user