(api) Add CRUD typebot endpoints

Closes #320, closes #696
This commit is contained in:
Baptiste Arnaud
2023-08-17 09:39:11 +02:00
parent 019f72ac7e
commit 454d320c6b
78 changed files with 25014 additions and 1073 deletions

View File

@@ -52,9 +52,11 @@ export const TypebotHeader = () => {
}, 1000)
const { isOpen, onOpen } = useDisclosure()
const handleNameSubmit = (name: string) => updateTypebot({ name })
const handleNameSubmit = (name: string) =>
updateTypebot({ updates: { name } })
const handleChangeIcon = (icon: string) => updateTypebot({ icon })
const handleChangeIcon = (icon: string) =>
updateTypebot({ updates: { icon } })
const handlePreviewClick = async () => {
setStartPreviewAtGroup(undefined)

View File

@@ -5,11 +5,6 @@ import {
createTypebots,
importTypebotInDatabase,
} from '@typebot.io/lib/playwright/databaseActions'
import {
waitForSuccessfulDeleteRequest,
waitForSuccessfulPostRequest,
waitForSuccessfulPutRequest,
} from '@typebot.io/lib/playwright/testHelpers'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import { getTestAsset } from '@/test/utils/playwright'
@@ -236,25 +231,13 @@ test('Published typebot menu should work', async ({ page }) => {
await expect(page.locator("text='Start'")).toBeVisible()
await expect(page.locator('button >> text="Published"')).toBeVisible()
await page.click('[aria-label="Show published typebot menu"]')
await Promise.all([
waitForSuccessfulPutRequest(page),
page.click('text="Close typebot to new responses"'),
])
await page.click('text="Close typebot to new responses"')
await expect(page.locator('button >> text="Closed"')).toBeDisabled()
await page.click('[aria-label="Show published typebot menu"]')
await Promise.all([
waitForSuccessfulPutRequest(page),
page.click('text="Reopen typebot to new responses"'),
])
await page.click('text="Reopen typebot to new responses"')
await expect(page.locator('button >> text="Published"')).toBeDisabled()
await page.click('[aria-label="Show published typebot menu"]')
await Promise.all([
waitForSuccessfulDeleteRequest(page),
page.click('button >> text="Unpublish typebot"'),
])
await Promise.all([
waitForSuccessfulPostRequest(page),
page.click('button >> text="Publish"'),
])
await page.click('button >> text="Unpublish typebot"')
await page.click('button >> text="Publish"')
await expect(page.locator('button >> text="Published"')).toBeVisible()
})

View File

@@ -1,9 +1,4 @@
import {
LogicBlockType,
PublicTypebot,
Typebot,
Webhook,
} from '@typebot.io/schemas'
import { LogicBlockType, PublicTypebot, Typebot } from '@typebot.io/schemas'
import { Router, useRouter } from 'next/router'
import {
createContext,
@@ -12,7 +7,6 @@ import {
useContext,
useEffect,
useMemo,
useState,
} from 'react'
import { isDefined, omit } from '@typebot.io/lib'
import { edgesAction, EdgesActions } from './typebotActions/edges'
@@ -22,21 +16,11 @@ import { blocksAction, BlocksActions } from './typebotActions/blocks'
import { variablesAction, VariablesActions } from './typebotActions/variables'
import { dequal } from 'dequal'
import { useToast } from '@/hooks/useToast'
import { useTypebotQuery } from '@/hooks/useTypebotQuery'
import { useUndo } from '../hooks/useUndo'
import { updateTypebotQuery } from '../queries/updateTypebotQuery'
import { updateWebhookQuery } from '@/features/blocks/integrations/webhook/queries/updateWebhookQuery'
import { useAutoSave } from '@/hooks/useAutoSave'
import { createWebhookQuery } from '@/features/blocks/integrations/webhook/queries/createWebhookQuery'
import { duplicateWebhookQuery } from '@/features/blocks/integrations/webhook/queries/duplicateWebhookQuery'
import { parseDefaultPublicId } from '@/features/publish/helpers/parseDefaultPublicId'
import { createPublishedTypebotQuery } from '@/features/publish/queries/createPublishedTypebotQuery'
import { deletePublishedTypebotQuery } from '@/features/publish/queries/deletePublishedTypebotQuery'
import { updatePublishedTypebotQuery } from '@/features/publish/queries/updatePublishedTypebotQuery'
import { preventUserFromRefreshing } from '@/helpers/preventUserFromRefreshing'
import { areTypebotsEqual } from '@/features/publish/helpers/areTypebotsEqual'
import { isPublished as isPublishedHelper } from '@/features/publish/helpers/isPublished'
import { convertTypebotToPublicTypebot } from '@/features/publish/helpers/convertTypebotToPublicTypebot'
import { convertPublicTypebotToTypebot } from '@/features/publish/helpers/convertPublicTypebotToTypebot'
import { trpc } from '@/lib/trpc'
@@ -66,23 +50,18 @@ const typebotContext = createContext<
typebot?: Typebot
publishedTypebot?: PublicTypebot
linkedTypebots?: Pick<Typebot, 'id' | 'groups' | 'variables' | 'name'>[]
webhooks: Webhook[]
isReadOnly?: boolean
isPublished: boolean
isPublishing: boolean
isSavingLoading: boolean
save: () => Promise<void>
save: () => Promise<Typebot | undefined>
undo: () => void
redo: () => void
canRedo: boolean
canUndo: boolean
updateWebhook: (
webhookId: string,
webhook: Partial<Webhook>
) => Promise<void>
updateTypebot: (updates: UpdateTypebotPayload) => void
publishTypebot: () => void
unpublishTypebot: () => void
updateTypebot: (props: {
updates: UpdateTypebotPayload
save?: boolean
}) => Promise<Typebot | undefined>
restorePublishedTypebot: () => void
} & GroupsActions &
BlocksActions &
@@ -104,15 +83,48 @@ export const TypebotProvider = ({
const { showToast } = useToast()
const {
typebot,
publishedTypebot,
webhooks,
isReadOnly,
data: typebotData,
isLoading: isFetchingTypebot,
mutate,
} = useTypebotQuery({
typebotId,
})
refetch: refetchTypebot,
} = trpc.typebot.getTypebot.useQuery(
{ typebotId: typebotId as string },
{
enabled: isDefined(typebotId),
onError: (error) =>
showToast({
title: 'Error while fetching typebot. Refresh the page.',
description: error.message,
}),
}
)
const { data: publishedTypebotData } =
trpc.typebot.getPublishedTypebot.useQuery(
{ typebotId: typebotId as string },
{
enabled: isDefined(typebotId),
onError: (error) =>
showToast({
title: 'Error while fetching published typebot',
description: error.message,
}),
}
)
const { mutateAsync: updateTypebot, isLoading: isSaving } =
trpc.typebot.updateTypebot.useMutation({
onError: (error) =>
showToast({
title: 'Error while updating typebot',
description: error.message,
}),
onSuccess: () => {
refetchTypebot()
},
})
const typebot = typebotData?.typebot
const publishedTypebot = publishedTypebotData?.publishedTypebot ?? undefined
const [
localTypebot,
@@ -180,53 +192,17 @@ export const TypebotProvider = ({
const typebotToSave = { ...localTypebot, ...updates }
if (dequal(omit(typebot, 'updatedAt'), omit(typebotToSave, 'updatedAt')))
return
setIsSavingLoading(true)
const { data, error } = await updateTypebotQuery(
typebotToSave.id,
typebotToSave
)
if (data?.typebot) setLocalTypebot({ ...data.typebot })
setIsSavingLoading(false)
if (error) {
showToast({ title: error.name, description: error.message })
return
}
mutate({
setLocalTypebot({ ...typebotToSave })
const { typebot: newTypebot } = await updateTypebot({
typebotId: typebotToSave.id,
typebot: typebotToSave,
publishedTypebot,
webhooks: webhooks ?? [],
})
window.removeEventListener('beforeunload', preventUserFromRefreshing)
setLocalTypebot({ ...newTypebot })
return newTypebot
},
[
localTypebot,
mutate,
publishedTypebot,
setLocalTypebot,
showToast,
typebot,
webhooks,
]
[localTypebot, setLocalTypebot, typebot, updateTypebot]
)
const savePublishedTypebot = async (newPublishedTypebot: PublicTypebot) => {
if (!localTypebot) return
setIsPublishing(true)
const { error } = await updatePublishedTypebotQuery(
newPublishedTypebot.id,
newPublishedTypebot,
localTypebot.workspaceId
)
setIsPublishing(false)
if (error)
return showToast({ title: error.name, description: error.message })
mutate({
typebot: localTypebot,
publishedTypebot: newPublishedTypebot,
webhooks: webhooks ?? [],
})
}
useAutoSave(
{
handler: saveTypebot,
@@ -246,12 +222,10 @@ export const TypebotProvider = ({
}
}, [saveTypebot])
const [isSavingLoading, setIsSavingLoading] = useState(false)
const [isPublishing, setIsPublishing] = useState(false)
const isPublished = useMemo(
() =>
isDefined(localTypebot) &&
isDefined(localTypebot.publicId) &&
isDefined(publishedTypebot) &&
isPublishedHelper(localTypebot, publishedTypebot),
[localTypebot, publishedTypebot]
@@ -268,56 +242,18 @@ export const TypebotProvider = ({
}
}, [localTypebot, typebot])
const updateLocalTypebot = (updates: UpdateTypebotPayload) =>
localTypebot && setLocalTypebot({ ...localTypebot, ...updates })
const publishTypebot = async () => {
const updateLocalTypebot = async ({
updates,
save,
}: {
updates: UpdateTypebotPayload
save?: boolean
}) => {
if (!localTypebot) return
const newLocalTypebot = { ...localTypebot }
if (!publishedTypebot || !localTypebot.publicId) {
const newPublicId =
localTypebot.publicId ??
parseDefaultPublicId(localTypebot.name, localTypebot.id)
newLocalTypebot.publicId = newPublicId
await saveTypebot({ publicId: newPublicId })
}
if (publishedTypebot) {
await savePublishedTypebot({
...convertTypebotToPublicTypebot(newLocalTypebot),
id: publishedTypebot.id,
})
} else {
setIsPublishing(true)
const { data, error } = await createPublishedTypebotQuery(
{
...omit(convertTypebotToPublicTypebot(newLocalTypebot), 'id'),
},
localTypebot.workspaceId
)
setIsPublishing(false)
if (error)
return showToast({ title: error.name, description: error.message })
mutate({
typebot: localTypebot,
publishedTypebot: data,
webhooks: webhooks ?? [],
})
}
}
const unpublishTypebot = async () => {
if (!publishedTypebot || !localTypebot) return
setIsPublishing(true)
const { error } = await deletePublishedTypebotQuery({
publishedTypebotId: publishedTypebot.id,
typebotId: localTypebot.id,
})
setIsPublishing(false)
if (error) showToast({ description: error.message })
mutate({
typebot: localTypebot,
webhooks: webhooks ?? [],
})
const newTypebot = { ...localTypebot, ...updates }
setLocalTypebot(newTypebot)
if (save) await saveTypebot(newTypebot)
return newTypebot
}
const restorePublishedTypebot = () => {
@@ -325,64 +261,6 @@ export const TypebotProvider = ({
setLocalTypebot(
convertPublicTypebotToTypebot(publishedTypebot, localTypebot)
)
return saveTypebot()
}
const updateWebhook = useCallback(
async (webhookId: string, updates: Partial<Webhook>) => {
if (!typebot) return
const { data } = await updateWebhookQuery({
typebotId: typebot.id,
webhookId,
data: updates,
})
if (data)
mutate({
typebot,
publishedTypebot,
webhooks: (webhooks ?? []).map((w) =>
w.id === webhookId ? data.webhook : w
),
})
},
[mutate, publishedTypebot, typebot, webhooks]
)
const createWebhook = async (data: Partial<Webhook>) => {
if (!typebot) return
const response = await createWebhookQuery({
typebotId: typebot.id,
data,
})
if (!response.data?.webhook) return
mutate({
typebot,
publishedTypebot,
webhooks: (webhooks ?? []).concat(response.data?.webhook),
})
}
const duplicateWebhook = async (
existingWebhookId: string,
newWebhookId: string
) => {
if (!typebot) return
const newWebhook = await duplicateWebhookQuery({
existingIds: {
typebotId: typebot.id,
webhookId: existingWebhookId,
},
newIds: {
typebotId: typebot.id,
webhookId: newWebhookId,
},
})
if (!newWebhook) return
mutate({
typebot,
publishedTypebot,
webhooks: (webhooks ?? []).concat(newWebhook),
})
}
return (
@@ -391,29 +269,18 @@ export const TypebotProvider = ({
typebot: localTypebot,
publishedTypebot,
linkedTypebots: linkedTypebotsData?.typebots ?? [],
webhooks: webhooks ?? [],
isReadOnly,
isSavingLoading,
isReadOnly: typebotData?.isReadOnly,
isSavingLoading: isSaving,
save: saveTypebot,
undo,
redo,
canUndo,
canRedo,
publishTypebot,
unpublishTypebot,
isPublishing,
isPublished,
updateTypebot: updateLocalTypebot,
restorePublishedTypebot,
updateWebhook,
...groupsActions(setLocalTypebot as SetTypebot, {
onWebhookBlockCreated: createWebhook,
onWebhookBlockDuplicated: duplicateWebhook,
}),
...blocksAction(setLocalTypebot as SetTypebot, {
onWebhookBlockCreated: createWebhook,
onWebhookBlockDuplicated: duplicateWebhook,
}),
...groupsActions(setLocalTypebot as SetTypebot),
...blocksAction(setLocalTypebot as SetTypebot),
...variablesAction(setLocalTypebot as SetTypebot),
...edgesAction(setLocalTypebot as SetTypebot),
...itemsAction(setLocalTypebot as SetTypebot),

View File

@@ -37,10 +37,7 @@ export type WebhookCallBacks = {
) => void
}
export const blocksAction = (
setTypebot: SetTypebot,
{ onWebhookBlockCreated, onWebhookBlockDuplicated }: WebhookCallBacks
): BlocksActions => ({
export const blocksAction = (setTypebot: SetTypebot): BlocksActions => ({
createBlock: (
groupId: string,
block: DraggableBlock | DraggableBlockType,
@@ -48,13 +45,7 @@ export const blocksAction = (
) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
createBlockDraft(
typebot,
block,
groupId,
indices,
onWebhookBlockCreated
)
createBlockDraft(typebot, block, groupId, indices)
})
),
updateBlock: (
@@ -74,10 +65,7 @@ export const blocksAction = (
const blocks = typebot.groups[groupIndex].blocks
if (blockIndex === blocks.length - 1 && block.outgoingEdgeId)
deleteEdgeDraft(typebot, block.outgoingEdgeId as string)
const newBlock = duplicateBlockDraft(block.groupId)(
block,
onWebhookBlockDuplicated
)
const newBlock = duplicateBlockDraft(block.groupId)(block)
typebot.groups[groupIndex].blocks.splice(blockIndex + 1, 0, newBlock)
})
),
@@ -105,8 +93,7 @@ export const createBlockDraft = (
typebot: Draft<Typebot>,
block: DraggableBlock | DraggableBlockType,
groupId: string,
{ groupIndex, blockIndex }: BlockIndices,
onWebhookBlockCreated?: (data: Partial<Webhook>) => void
{ groupIndex, blockIndex }: BlockIndices
) => {
const blocks = typebot.groups[groupIndex].blocks
if (
@@ -116,13 +103,7 @@ export const createBlockDraft = (
)
deleteEdgeDraft(typebot, blocks[blockIndex - 1].outgoingEdgeId as string)
typeof block === 'string'
? createNewBlock(
typebot,
block,
groupId,
{ groupIndex, blockIndex },
onWebhookBlockCreated
)
? createNewBlock(typebot, block, groupId, { groupIndex, blockIndex })
: moveBlockToGroup(typebot, block, groupId, { groupIndex, blockIndex })
removeEmptyGroups(typebot)
}
@@ -174,10 +155,7 @@ const moveBlockToGroup = (
export const duplicateBlockDraft =
(groupId: string) =>
(
block: Block,
onWebhookBlockDuplicated: WebhookCallBacks['onWebhookBlockDuplicated']
): Block => {
(block: Block): Block => {
const blockId = createId()
if (blockHasItems(block))
return {
@@ -189,8 +167,6 @@ export const duplicateBlockDraft =
} as Block
if (isWebhookBlock(block)) {
const newWebhookId = createId()
if (block.webhookId)
onWebhookBlockDuplicated(block.webhookId, newWebhookId)
return {
...block,
groupId,

View File

@@ -11,7 +11,6 @@ import {
deleteGroupDraft,
createBlockDraft,
duplicateBlockDraft,
WebhookCallBacks,
} from './blocks'
import { isEmpty, parseGroupTitle } from '@typebot.io/lib'
import { Coordinates } from '@/features/graph/types'
@@ -29,10 +28,7 @@ export type GroupsActions = {
deleteGroup: (groupIndex: number) => void
}
const groupsActions = (
setTypebot: SetTypebot,
{ onWebhookBlockCreated, onWebhookBlockDuplicated }: WebhookCallBacks
): GroupsActions => ({
const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({
createGroup: ({
id,
block,
@@ -52,13 +48,7 @@ const groupsActions = (
blocks: [],
}
typebot.groups.push(newGroup)
createBlockDraft(
typebot,
block,
newGroup.id,
indices,
onWebhookBlockCreated
)
createBlockDraft(typebot, block, newGroup.id, indices)
})
),
updateGroup: (groupIndex: number, updates: Partial<Omit<Group, 'id'>>) =>
@@ -79,9 +69,7 @@ const groupsActions = (
? ''
: `${parseGroupTitle(group.title)} copy`,
id,
blocks: group.blocks.map((block) =>
duplicateBlockDraft(id)(block, onWebhookBlockDuplicated)
),
blocks: group.blocks.map((block) => duplicateBlockDraft(id)(block)),
graphCoordinates: {
x: group.graphCoordinates.x + 200,
y: group.graphCoordinates.y + 100,

View File

@@ -1,9 +0,0 @@
import { Typebot } from '@typebot.io/schemas'
import { sendRequest } from '@typebot.io/lib'
export const updateTypebotQuery = async (id: string, typebot: Typebot) =>
sendRequest<{ typebot: Typebot }>({
url: `/api/typebots/${id}`,
method: 'PUT',
body: typebot,
})