♻️ Introduce typebot v6 with events (#1013)

Closes #885
This commit is contained in:
Baptiste Arnaud
2023-11-08 15:34:16 +01:00
committed by GitHub
parent 68e4fc71fb
commit 35300eaf34
634 changed files with 58971 additions and 31449 deletions

View File

@@ -1,11 +1,4 @@
import { Flex, HStack, Tooltip, useColorModeValue } from '@chakra-ui/react'
import {
BubbleBlockType,
DraggableBlockType,
InputBlockType,
IntegrationBlockType,
LogicBlockType,
} from '@typebot.io/schemas'
import { useBlockDnd } from '@/features/graph/providers/GraphDndProvider'
import React, { useEffect, useState } from 'react'
import { BlockIcon } from './BlockIcon'
@@ -15,13 +8,18 @@ import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { BlockLabel } from './BlockLabel'
import { LockTag } from '@/features/billing/components/LockTag'
import { useTranslate } from '@tolgee/react'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
import { BlockV6 } from '@typebot.io/schemas'
type Props = {
type: DraggableBlockType
type: BlockV6['type']
tooltip?: string
isDisabled?: boolean
children: React.ReactNode
onMouseDown: (e: React.MouseEvent, type: DraggableBlockType) => void
onMouseDown: (e: React.MouseEvent, type: BlockV6['type']) => void
}
export const BlockCard = (

View File

@@ -1,12 +1,12 @@
import { StackProps, HStack, useColorModeValue } from '@chakra-ui/react'
import { BlockType } from '@typebot.io/schemas'
import { BlockIcon } from './BlockIcon'
import { BlockLabel } from './BlockLabel'
import { BlockV6 } from '@typebot.io/schemas'
export const BlockCardOverlay = ({
type,
...props
}: StackProps & { type: BlockType }) => {
}: StackProps & { type: BlockV6['type'] }) => {
return (
<HStack
borderWidth="1px"

View File

@@ -1,11 +1,4 @@
import { IconProps, useColorModeValue } from '@chakra-ui/react'
import {
BubbleBlockType,
InputBlockType,
IntegrationBlockType,
LogicBlockType,
BlockType,
} from '@typebot.io/schemas'
import React from 'react'
import { FlagIcon, SendEmailIcon, WebhookIcon } from '@/components/icons'
import { WaitIcon } from '@/features/blocks/logic/wait/components/WaitIcon'
@@ -41,8 +34,13 @@ import { AbTestIcon } from '@/features/blocks/logic/abTest/components/AbTestIcon
import { PictureChoiceIcon } from '@/features/blocks/inputs/pictureChoice/components/PictureChoiceIcon'
import { PixelLogo } from '@/features/blocks/integrations/pixel/components/PixelLogo'
import { ZemanticAiLogo } from '@/features/blocks/integrations/zemanticAi/ZemanticAiLogo'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
import { Block } from '@typebot.io/schemas'
type BlockIconProps = { type: BlockType } & IconProps
type BlockIconProps = { type: Block['type'] } & IconProps
export const BlockIcon = ({ type, ...props }: BlockIconProps): JSX.Element => {
const blue = useColorModeValue('blue.500', 'blue.300')

View File

@@ -1,15 +1,13 @@
import { Text } from '@chakra-ui/react'
import {
BubbleBlockType,
InputBlockType,
IntegrationBlockType,
LogicBlockType,
BlockType,
} from '@typebot.io/schemas'
import React from 'react'
import { useTranslate } from '@tolgee/react'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
import { Block } from '@typebot.io/schemas'
type Props = { type: BlockType }
type Props = { type: Block['type'] }
export const BlockLabel = ({ type }: Props): JSX.Element => {
const { t } = useTranslate()

View File

@@ -10,13 +10,6 @@ import {
Fade,
useColorModeValue,
} from '@chakra-ui/react'
import {
BubbleBlockType,
DraggableBlockType,
InputBlockType,
IntegrationBlockType,
LogicBlockType,
} from '@typebot.io/schemas'
import { useBlockDnd } from '@/features/graph/providers/GraphDndProvider'
import React, { useState } from 'react'
import { BlockCard } from './BlockCard'
@@ -24,6 +17,11 @@ import { LockedIcon, UnlockedIcon } from '@/components/icons'
import { BlockCardOverlay } from './BlockCardOverlay'
import { headerHeight } from '../constants'
import { useTranslate } from '@tolgee/react'
import { BubbleBlockType } from '@typebot.io/schemas/features/blocks/bubbles/constants'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
import { BlockV6 } from '@typebot.io/schemas'
export const BlocksSideBar = () => {
const { t } = useTranslate()
@@ -47,7 +45,7 @@ export const BlocksSideBar = () => {
}
useEventListener('mousemove', handleMouseMove)
const handleMouseDown = (e: React.MouseEvent, type: DraggableBlockType) => {
const handleMouseDown = (e: React.MouseEvent, type: BlockV6['type']) => {
const element = e.currentTarget as HTMLDivElement
const rect = element.getBoundingClientRect()
setPosition({ x: rect.left, y: rect.top })

View File

@@ -15,6 +15,7 @@ import { Graph } from '@/features/graph/components/Graph'
import { GraphDndProvider } from '@/features/graph/providers/GraphDndProvider'
import { GraphProvider } from '@/features/graph/providers/GraphProvider'
import { GroupsCoordinatesProvider } from '@/features/graph/providers/GroupsCoordinateProvider'
import { EventsCoordinatesProvider } from '@/features/graph/providers/EventsCoordinateProvider'
export const EditorPage = () => {
const { typebot, isReadOnly } = useTypebot()
@@ -42,9 +43,11 @@ export const EditorPage = () => {
{!isReadOnly && <BlocksSideBar />}
<GraphProvider isReadOnly={isReadOnly}>
<GroupsCoordinatesProvider groups={typebot.groups}>
<Graph flex="1" typebot={typebot} key={typebot.id} />
<BoardMenuButton pos="absolute" right="40px" top="20px" />
<RightPanel />
<EventsCoordinatesProvider events={typebot.events}>
<Graph flex="1" typebot={typebot} key={typebot.id} />
<BoardMenuButton pos="absolute" right="40px" top="20px" />
<RightPanel />
</EventsCoordinatesProvider>
</GroupsCoordinatesProvider>
</GraphProvider>
</GraphDndProvider>

View File

@@ -46,7 +46,12 @@ export const TypebotHeader = () => {
canRedo,
isSavingLoading,
} = useTypebot()
const { setRightPanel, rightPanel, setStartPreviewAtGroup } = useEditor()
const {
setRightPanel,
rightPanel,
setStartPreviewAtGroup,
setStartPreviewAtEvent,
} = useEditor()
const [isUndoShortcutTooltipOpen, setUndoShortcutTooltipOpen] =
useState(false)
const hideUndoShortcutTooltipLater = useDebouncedCallback(() => {
@@ -62,6 +67,7 @@ export const TypebotHeader = () => {
const handlePreviewClick = async () => {
setStartPreviewAtGroup(undefined)
setStartPreviewAtEvent(undefined)
save().then()
setRightPanel(RightPanel.PREVIEW)
}

View File

@@ -1,5 +1,4 @@
import test, { expect } from '@playwright/test'
import { defaultTextInputOptions, InputBlockType } from '@typebot.io/schemas'
import { createId } from '@paralleldrive/cuid2'
import {
createTypebots,
@@ -7,6 +6,7 @@ import {
} from '@typebot.io/lib/playwright/databaseActions'
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
import { getTestAsset } from '@/test/utils/playwright'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
test.describe.configure({ mode: 'parallel' })
@@ -24,20 +24,20 @@ test('Edges connection should work', async ({ page }) => {
})
await page.dragAndDrop(
'text=Text >> nth=0',
'[data-testid="group"] >> nth=1',
'[data-testid="group"] >> nth=0',
{
targetPosition: { x: 100, y: 50 },
}
)
await page.dragAndDrop(
'[data-testid="endpoint"]',
'[data-testid="group"] >> nth=1',
'[data-testid="group"] >> nth=0',
{ targetPosition: { x: 100, y: 10 } }
)
await expect(page.locator('[data-testid="edge"]')).toBeVisible()
await page.dragAndDrop(
'[data-testid="endpoint"]',
'[data-testid="group"] >> nth=1'
'[data-testid="group"] >> nth=0'
)
await expect(page.locator('[data-testid="edge"]')).toBeVisible()
await page.dragAndDrop('text=Date', '#editor-container', {
@@ -45,7 +45,7 @@ test('Edges connection should work', async ({ page }) => {
})
await page.dragAndDrop(
'[data-testid="endpoint"] >> nth=2',
'[data-testid="group"] >> nth=2',
'[data-testid="group"] >> nth=1',
{
targetPosition: { x: 100, y: 10 },
}
@@ -72,17 +72,17 @@ test('Drag and drop blocks and items should work', async ({ page }) => {
// Blocks dnd
await page.goto(`/typebots/${typebotId}/edit`)
await expect(page.locator('[data-testid="block"] >> nth=0')).toHaveText(
'Hello!'
)
await page.dragAndDrop('text=Hello', '[data-testid="block"] >> nth=2', {
targetPosition: { x: 100, y: 0 },
})
await expect(page.locator('[data-testid="block"] >> nth=1')).toHaveText(
'Hello!'
)
await page.dragAndDrop('text=Hello', '[data-testid="block"] >> nth=3', {
targetPosition: { x: 100, y: 0 },
})
await expect(page.locator('[data-testid="block"] >> nth=2')).toHaveText(
'Hello!'
)
await page.dragAndDrop('text=Hello', 'text=Group #2')
await expect(page.locator('[data-testid="block"] >> nth=3')).toHaveText(
await expect(page.locator('[data-testid="block"] >> nth=2')).toHaveText(
'Hello!'
)
@@ -120,7 +120,6 @@ test('Undo / Redo and Zoom buttons should work', async ({ page }) => {
id: typebotId,
...parseDefaultGroupWithBlock({
type: InputBlockType.TEXT,
options: defaultTextInputOptions,
}),
},
])
@@ -129,7 +128,7 @@ test('Undo / Redo and Zoom buttons should work', async ({ page }) => {
await page.click('text=Group #1', { button: 'right' })
await page.click('text=Duplicate')
await expect(page.locator('text="Group #1"')).toBeVisible()
await expect(page.locator('text="Group #1 copy"')).toBeVisible()
await expect(page.locator('text="Group #1 (1)"')).toBeVisible()
await page.click('text="Group #1"', { button: 'right' })
await page.click('text=Delete')
await expect(page.locator('text="Group #1"')).toBeHidden()
@@ -140,18 +139,18 @@ test('Undo / Redo and Zoom buttons should work', async ({ page }) => {
await page.getByRole('button', { name: 'Zoom in' }).click()
await expect(page.getByTestId('graph')).toHaveAttribute(
'style',
/scale\(1\.2\);$/
/scale\(1\.2\)/
)
await page.getByRole('button', { name: 'Zoom in' }).click()
await expect(page.getByTestId('graph')).toHaveAttribute(
'style',
/scale\(1\.4\);$/
/scale\(1\.4\)/
)
await page.getByRole('button', { name: 'Zoom out' }).dblclick()
await page.getByRole('button', { name: 'Zoom out' }).dblclick()
await expect(page.getByTestId('graph')).toHaveAttribute(
'style',
/scale\(0\.6\);$/
/scale\(0\.6\)/
)
})
@@ -163,7 +162,6 @@ test('Rename and icon change should work', async ({ page }) => {
name: 'My awesome typebot',
...parseDefaultGroupWithBlock({
type: InputBlockType.TEXT,
options: defaultTextInputOptions,
}),
},
])
@@ -194,7 +192,7 @@ test('Preview from group should work', async ({ page }) => {
await page.goto(`/typebots/${typebotId}/edit`)
await page
.getByTestId('group')
.nth(1)
.nth(0)
.click({ position: { x: 100, y: 10 } })
await page.click('[aria-label="Preview bot from this group"]')
await expect(
@@ -202,7 +200,7 @@ test('Preview from group should work', async ({ page }) => {
).toBeVisible()
await page
.getByTestId('group')
.nth(2)
.nth(1)
.click({ position: { x: 100, y: 10 } })
await page.click('[aria-label="Preview bot from this group"]')
await expect(
@@ -223,8 +221,8 @@ test('Published typebot menu should work', async ({ page }) => {
name: 'My awesome typebot',
...parseDefaultGroupWithBlock({
type: InputBlockType.TEXT,
options: defaultTextInputOptions,
}),
version: '6',
},
])
await page.goto(`/typebots/${typebotId}/edit`)

View File

@@ -16,6 +16,8 @@ const editorContext = createContext<{
setRightPanel: Dispatch<SetStateAction<RightPanel | undefined>>
startPreviewAtGroup: string | undefined
setStartPreviewAtGroup: Dispatch<SetStateAction<string | undefined>>
startPreviewAtEvent: string | undefined
setStartPreviewAtEvent: Dispatch<SetStateAction<string | undefined>>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
}>({})
@@ -23,6 +25,7 @@ const editorContext = createContext<{
export const EditorProvider = ({ children }: { children: ReactNode }) => {
const [rightPanel, setRightPanel] = useState<RightPanel>()
const [startPreviewAtGroup, setStartPreviewAtGroup] = useState<string>()
const [startPreviewAtEvent, setStartPreviewAtEvent] = useState<string>()
return (
<editorContext.Provider
@@ -31,6 +34,8 @@ export const EditorProvider = ({ children }: { children: ReactNode }) => {
setRightPanel,
startPreviewAtGroup,
setStartPreviewAtGroup,
startPreviewAtEvent,
setStartPreviewAtEvent,
}}
>
{children}

View File

@@ -1,4 +1,4 @@
import { PublicTypebot, Typebot } from '@typebot.io/schemas'
import { PublicTypebot, PublicTypebotV6, TypebotV6 } from '@typebot.io/schemas'
import { Router, useRouter } from 'next/router'
import {
createContext,
@@ -23,12 +23,13 @@ import { areTypebotsEqual } from '@/features/publish/helpers/areTypebotsEqual'
import { isPublished as isPublishedHelper } from '@/features/publish/helpers/isPublished'
import { convertPublicTypebotToTypebot } from '@/features/publish/helpers/convertPublicTypebotToTypebot'
import { trpc } from '@/lib/trpc'
import { EventsActions, eventsActions } from './typebotActions/events'
const autoSaveTimeout = 10000
type UpdateTypebotPayload = Partial<
Pick<
Typebot,
TypebotV6,
| 'theme'
| 'selectedThemeTemplateId'
| 'settings'
@@ -43,17 +44,18 @@ type UpdateTypebotPayload = Partial<
>
export type SetTypebot = (
newPresent: Typebot | ((current: Typebot) => Typebot)
newPresent: TypebotV6 | ((current: TypebotV6) => TypebotV6)
) => void
const typebotContext = createContext<
{
typebot?: Typebot
publishedTypebot?: PublicTypebot
typebot?: TypebotV6
publishedTypebot?: PublicTypebotV6
publishedTypebotVersion?: PublicTypebot['version']
isReadOnly?: boolean
isPublished: boolean
isSavingLoading: boolean
save: () => Promise<Typebot | undefined>
save: () => Promise<TypebotV6 | undefined>
undo: () => void
redo: () => void
canRedo: boolean
@@ -61,13 +63,14 @@ const typebotContext = createContext<
updateTypebot: (props: {
updates: UpdateTypebotPayload
save?: boolean
}) => Promise<Typebot | undefined>
}) => Promise<TypebotV6 | undefined>
restorePublishedTypebot: () => void
} & GroupsActions &
BlocksActions &
ItemsActions &
VariablesActions &
EdgesActions
EdgesActions &
EventsActions
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
>({})
@@ -87,27 +90,50 @@ export const TypebotProvider = ({
isLoading: isFetchingTypebot,
refetch: refetchTypebot,
} = trpc.typebot.getTypebot.useQuery(
{ typebotId: typebotId as string },
{ typebotId: typebotId as string, migrateToLatestVersion: true },
{
enabled: isDefined(typebotId),
retry: 0,
onError: (error) => {
if (error.data?.httpStatus === 404) {
showToast({
status: 'info',
description: "Couldn't find typebot.",
description: "Couldn't find typebot. Redirecting...",
})
push('/typebots')
return
}
showToast({
title: 'Could not fetch typebot',
description: error.message,
details: {
content: JSON.stringify(error.data?.zodError?.fieldErrors, null, 2),
lang: 'json',
},
})
},
}
)
const { data: publishedTypebotData } =
trpc.typebot.getPublishedTypebot.useQuery(
{ typebotId: typebotId as string },
{ typebotId: typebotId as string, migrateToLatestVersion: true },
{
enabled: isDefined(typebotId),
onError: (error) => {
showToast({
title: 'Could not fetch published typebot',
description: error.message,
details: {
content: JSON.stringify(
error.data?.zodError?.fieldErrors,
null,
2
),
lang: 'json',
},
})
},
}
)
@@ -124,13 +150,14 @@ export const TypebotProvider = ({
},
})
const typebot = typebotData?.typebot
const publishedTypebot = publishedTypebotData?.publishedTypebot ?? undefined
const typebot = typebotData?.typebot as TypebotV6
const publishedTypebot = (publishedTypebotData?.publishedTypebot ??
undefined) as PublicTypebotV6 | undefined
const [
localTypebot,
{ redo, undo, flush, canRedo, canUndo, set: setLocalTypebot },
] = useUndo<Typebot>(undefined)
] = useUndo<TypebotV6>(undefined)
useEffect(() => {
if (!typebot && isDefined(localTypebot)) setLocalTypebot(undefined)
@@ -154,7 +181,7 @@ export const TypebotProvider = ({
])
const saveTypebot = useCallback(
async (updates?: Partial<Typebot>) => {
async (updates?: Partial<TypebotV6>) => {
if (!localTypebot || !typebot || typebotData?.isReadOnly) return
const typebotToSave = { ...localTypebot, ...updates }
if (dequal(omit(typebot, 'updatedAt'), omit(typebotToSave, 'updatedAt')))
@@ -241,6 +268,7 @@ export const TypebotProvider = ({
value={{
typebot: localTypebot,
publishedTypebot,
publishedTypebotVersion: publishedTypebotData?.version,
isReadOnly: typebotData?.isReadOnly,
isSavingLoading: isSaving,
save: saveTypebot,
@@ -256,6 +284,7 @@ export const TypebotProvider = ({
...variablesAction(setLocalTypebot as SetTypebot),
...edgesAction(setLocalTypebot as SetTypebot),
...itemsAction(setLocalTypebot as SetTypebot),
...eventsActions(setLocalTypebot as SetTypebot),
}}
>
{children}

View File

@@ -1,28 +1,24 @@
import {
Block,
Typebot,
DraggableBlock,
DraggableBlockType,
BlockIndices,
Webhook,
BlockV6,
TypebotV6,
} from '@typebot.io/schemas'
import { SetTypebot } from '../TypebotProvider'
import { produce, Draft } from 'immer'
import { cleanUpEdgeDraft, deleteEdgeDraft } from './edges'
import { createId } from '@paralleldrive/cuid2'
import { byId, isWebhookBlock, blockHasItems } from '@typebot.io/lib'
import { byId, blockHasItems } from '@typebot.io/lib'
import { duplicateItemDraft } from './items'
import { parseNewBlock } from '@/features/typebot/helpers/parseNewBlock'
export type BlocksActions = {
createBlock: (
groupId: string,
block: DraggableBlock | DraggableBlockType,
indices: BlockIndices
) => void
createBlock: (block: BlockV6 | BlockV6['type'], indices: BlockIndices) => void
updateBlock: (
indices: BlockIndices,
updates: Partial<Omit<Block, 'id' | 'type'>>
updates: Partial<Omit<BlockV6, 'id' | 'type'>>
) => void
duplicateBlock: (indices: BlockIndices) => void
detachBlockFromGroup: (indices: BlockIndices) => void
@@ -38,14 +34,10 @@ export type WebhookCallBacks = {
}
export const blocksAction = (setTypebot: SetTypebot): BlocksActions => ({
createBlock: (
groupId: string,
block: DraggableBlock | DraggableBlockType,
indices: BlockIndices
) =>
createBlock: (block: BlockV6 | BlockV6['type'], indices: BlockIndices) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
createBlockDraft(typebot, block, groupId, indices)
createBlockDraft(typebot, block, indices)
})
),
updateBlock: (
@@ -64,8 +56,8 @@ export const blocksAction = (setTypebot: SetTypebot): BlocksActions => ({
const block = { ...typebot.groups[groupIndex].blocks[blockIndex] }
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)
deleteEdgeDraft({ typebot, edgeId: block.outgoingEdgeId })
const newBlock = duplicateBlockDraft(block)
typebot.groups[groupIndex].blocks.splice(blockIndex + 1, 0, newBlock)
})
),
@@ -84,15 +76,13 @@ export const blocksAction = (setTypebot: SetTypebot): BlocksActions => ({
const removeBlockFromGroup =
({ groupIndex, blockIndex }: BlockIndices) =>
(typebot: Draft<Typebot>) => {
if (typebot.groups[groupIndex].blocks[blockIndex].type === 'start') return
(typebot: Draft<TypebotV6>) => {
typebot.groups[groupIndex].blocks.splice(blockIndex, 1)
}
export const createBlockDraft = (
typebot: Draft<Typebot>,
block: DraggableBlock | DraggableBlockType,
groupId: string,
typebot: Draft<TypebotV6>,
block: BlockV6 | BlockV6['type'],
{ groupIndex, blockIndex }: BlockIndices
) => {
const blocks = typebot.groups[groupIndex].blocks
@@ -101,50 +91,42 @@ export const createBlockDraft = (
blockIndex > 0 &&
blocks[blockIndex - 1].outgoingEdgeId
)
deleteEdgeDraft(typebot, blocks[blockIndex - 1].outgoingEdgeId as string)
deleteEdgeDraft({
typebot,
edgeId: blocks[blockIndex - 1].outgoingEdgeId as string,
groupIndex,
})
typeof block === 'string'
? createNewBlock(typebot, block, groupId, { groupIndex, blockIndex })
: moveBlockToGroup(typebot, block, groupId, { groupIndex, blockIndex })
? createNewBlock(typebot, block, { groupIndex, blockIndex })
: moveBlockToGroup(typebot, block, { groupIndex, blockIndex })
removeEmptyGroups(typebot)
}
const createNewBlock = async (
typebot: Draft<Typebot>,
type: DraggableBlockType,
groupId: string,
{ groupIndex, blockIndex }: BlockIndices,
onWebhookBlockCreated?: (data: Partial<Webhook>) => void
type: BlockV6['type'],
{ groupIndex, blockIndex }: BlockIndices
) => {
const newBlock = parseNewBlock(type, groupId)
const newBlock = parseNewBlock(type)
typebot.groups[groupIndex].blocks.splice(blockIndex ?? 0, 0, newBlock)
if (onWebhookBlockCreated && 'webhookId' in newBlock && newBlock.webhookId)
onWebhookBlockCreated({ id: newBlock.webhookId })
}
const moveBlockToGroup = (
typebot: Draft<Typebot>,
block: DraggableBlock,
groupId: string,
typebot: Draft<TypebotV6>,
block: BlockV6,
{ groupIndex, blockIndex }: BlockIndices
) => {
const newBlock = { ...block, groupId }
const items = blockHasItems(block) ? block.items : []
items.forEach((item) => {
const edgeIndex = typebot.edges.findIndex(byId(item.outgoingEdgeId))
if (edgeIndex === -1) return
typebot.edges[edgeIndex].from.groupId = groupId
})
const newBlock = { ...block }
if (block.outgoingEdgeId) {
if (typebot.groups[groupIndex].blocks.length > blockIndex ?? 0) {
deleteEdgeDraft(typebot, block.outgoingEdgeId)
deleteEdgeDraft({ typebot, edgeId: block.outgoingEdgeId })
newBlock.outgoingEdgeId = undefined
} else {
const edgeIndex = typebot.edges.findIndex(byId(block.outgoingEdgeId))
edgeIndex !== -1
? (typebot.edges[edgeIndex].from.groupId = groupId)
: (newBlock.outgoingEdgeId = undefined)
if (edgeIndex === -1) newBlock.outgoingEdgeId = undefined
}
}
const groupId = typebot.groups[groupIndex].id
typebot.edges.forEach((edge) => {
if (edge.to.blockId === block.id) {
edge.to.groupId = groupId
@@ -153,44 +135,29 @@ const moveBlockToGroup = (
typebot.groups[groupIndex].blocks.splice(blockIndex ?? 0, 0, newBlock)
}
export const duplicateBlockDraft =
(groupId: string) =>
(block: Block): Block => {
const blockId = createId()
if (blockHasItems(block))
return {
...block,
groupId,
id: blockId,
items: block.items.map(duplicateItemDraft(blockId)),
outgoingEdgeId: undefined,
} as Block
if (isWebhookBlock(block)) {
const newWebhookId = createId()
return {
...block,
groupId,
id: blockId,
webhookId: newWebhookId,
outgoingEdgeId: undefined,
}
}
export const duplicateBlockDraft = (block: BlockV6): BlockV6 => {
const blockId = createId()
if (blockHasItems(block))
return {
...block,
groupId,
id: blockId,
items: block.items?.map(duplicateItemDraft(blockId)),
outgoingEdgeId: undefined,
}
} as BlockV6
return {
...block,
id: blockId,
outgoingEdgeId: undefined,
}
}
export const deleteGroupDraft =
(typebot: Draft<Typebot>) => (groupIndex: number) => {
if (typebot.groups[groupIndex].blocks.at(0)?.type === 'start') return
(typebot: Draft<TypebotV6>) => (groupIndex: number) => {
cleanUpEdgeDraft(typebot, typebot.groups[groupIndex].id)
typebot.groups.splice(groupIndex, 1)
}
export const removeEmptyGroups = (typebot: Draft<Typebot>) => {
export const removeEmptyGroups = (typebot: Draft<TypebotV6>) => {
const emptyGroupsIndices = typebot.groups.reduce<number[]>(
(arr, group, idx) => {
group.blocks.length === 0 && arr.push(idx)

View File

@@ -5,6 +5,7 @@ import {
BlockIndices,
ItemIndices,
Block,
TypebotV6,
} from '@typebot.io/schemas'
import { SetTypebot } from '../TypebotProvider'
import { Draft, produce } from 'immer'
@@ -27,28 +28,53 @@ export const edgesAction = (setTypebot: SetTypebot): EdgesActions => ({
}
removeExistingEdge(typebot, edge)
typebot.edges.push(newEdge)
const groupIndex = typebot.groups.findIndex(byId(edge.from.groupId))
const blockIndex = typebot.groups[groupIndex].blocks.findIndex(
byId(edge.from.blockId)
)
const itemIndex = edge.from.itemId
? (
typebot.groups[groupIndex].blocks[blockIndex] as
| BlockWithItems
| undefined
)?.items.findIndex(byId(edge.from.itemId))
: null
if ('eventId' in edge.from) {
const eventIndex = typebot.events.findIndex(byId(edge.from.eventId))
addEdgeIdToEvent(typebot, newEdge.id, {
eventIndex,
})
} else {
const groupIndex = typebot.groups.findIndex((g) =>
g.blocks.some(
(b) => 'blockId' in edge.from && b.id === edge.from.blockId
)
)
const blockIndex = typebot.groups[groupIndex].blocks.findIndex(
byId(edge.from.blockId)
)
const itemIndex = edge.from.itemId
? (
typebot.groups[groupIndex].blocks[blockIndex] as
| BlockWithItems
| undefined
)?.items.findIndex(byId(edge.from.itemId))
: null
isDefined(itemIndex) && itemIndex !== -1
? addEdgeIdToItem(typebot, newEdge.id, {
groupIndex,
blockIndex,
itemIndex,
})
: addEdgeIdToBlock(typebot, newEdge.id, {
groupIndex,
blockIndex,
})
isDefined(itemIndex) && itemIndex !== -1
? addEdgeIdToItem(typebot, newEdge.id, {
groupIndex,
blockIndex,
itemIndex,
})
: addEdgeIdToBlock(typebot, newEdge.id, {
groupIndex,
blockIndex,
})
const block = typebot.groups[groupIndex].blocks[blockIndex]
if (isDefined(itemIndex) && isDefined(block.outgoingEdgeId)) {
const areAllItemsConnected = (block as BlockWithItems).items.every(
(item) => isDefined(item.outgoingEdgeId)
)
if (areAllItemsConnected) {
deleteEdgeDraft({
typebot,
edgeId: block.outgoingEdgeId,
groupIndex,
})
}
}
}
})
),
updateEdge: (edgeIndex: number, updates: Partial<Omit<Edge, 'id'>>) =>
@@ -64,11 +90,17 @@ export const edgesAction = (setTypebot: SetTypebot): EdgesActions => ({
deleteEdge: (edgeId: string) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
deleteEdgeDraft(typebot, edgeId)
deleteEdgeDraft({ typebot, edgeId })
})
),
})
const addEdgeIdToEvent = (
typebot: Draft<TypebotV6>,
edgeId: string,
{ eventIndex }: { eventIndex: number }
) => (typebot.events[eventIndex].outgoingEdgeId = edgeId)
const addEdgeIdToBlock = (
typebot: Draft<Typebot>,
edgeId: string,
@@ -86,17 +118,43 @@ const addEdgeIdToItem = (
itemIndex
].outgoingEdgeId = edgeId)
export const deleteEdgeDraft = (typebot: Draft<Typebot>, edgeId: string) => {
export const deleteEdgeDraft = ({
typebot,
edgeId,
groupIndex,
}: {
typebot: Draft<TypebotV6>
edgeId: string
groupIndex?: number
}) => {
const edgeIndex = typebot.edges.findIndex(byId(edgeId))
if (edgeIndex === -1) return
deleteOutgoingEdgeIdProps(typebot, edgeId)
deleteOutgoingEdgeIdProps({ typebot, edgeId, groupIndex })
typebot.edges.splice(edgeIndex, 1)
}
const deleteOutgoingEdgeIdProps = (typebot: Draft<Typebot>, edgeId: string) => {
const deleteOutgoingEdgeIdProps = ({
typebot,
edgeId,
groupIndex,
}: {
typebot: Draft<TypebotV6>
edgeId: string
groupIndex?: number
}) => {
const edge = typebot.edges.find(byId(edgeId))
if (!edge) return
const fromGroupIndex = typebot.groups.findIndex(byId(edge.from.groupId))
if ('eventId' in edge.from) {
const eventIndex = typebot.events.findIndex(byId(edge.from.eventId))
if (eventIndex === -1) return
typebot.events[eventIndex].outgoingEdgeId = undefined
return
}
const fromGroupIndex =
groupIndex ??
typebot.groups.findIndex((g) =>
g.blocks.some((b) => 'blockId' in edge.from && b.id === edge.from.blockId)
)
const fromBlockIndex = typebot.groups[fromGroupIndex].blocks.findIndex(
byId(edge.from.blockId)
)
@@ -106,40 +164,52 @@ const deleteOutgoingEdgeIdProps = (typebot: Draft<Typebot>, edgeId: string) => {
if (!block) return
const fromItemIndex =
edge.from.itemId && blockHasItems(block)
? block.items.findIndex(byId(edge.from.itemId))
? block.items?.findIndex(byId(edge.from.itemId))
: -1
if (fromItemIndex !== -1) {
;(
typebot.groups[fromGroupIndex].blocks[fromBlockIndex] as BlockWithItems
).items[fromItemIndex].outgoingEdgeId = undefined
).items[fromItemIndex ?? 0].outgoingEdgeId = undefined
} else if (fromBlockIndex !== -1)
typebot.groups[fromGroupIndex].blocks[fromBlockIndex].outgoingEdgeId =
undefined
}
export const cleanUpEdgeDraft = (
typebot: Draft<Typebot>,
typebot: Draft<TypebotV6>,
deletedNodeId: string
) => {
const edgesToDelete = typebot.edges.filter((edge) =>
[
edge.from.groupId,
const edgesToDelete = typebot.edges.filter((edge) => {
if ('eventId' in edge.from)
return [edge.from.eventId, edge.to.groupId, edge.to.blockId].includes(
deletedNodeId
)
return [
edge.from.blockId,
edge.from.itemId,
edge.to.groupId,
edge.to.blockId,
].includes(deletedNodeId)
)
edgesToDelete.forEach((edge) => deleteEdgeDraft(typebot, edge.id))
})
edgesToDelete.forEach((edge) => deleteEdgeDraft({ typebot, edgeId: edge.id }))
}
const removeExistingEdge = (
typebot: Draft<Typebot>,
edge: Omit<Edge, 'id'>
) => {
typebot.edges = typebot.edges.filter((e) =>
edge.from.itemId
? e.from.itemId !== edge.from.itemId
typebot.edges = typebot.edges.filter((e) => {
if ('eventId' in edge.from) {
if ('eventId' in e.from) return e.from.eventId !== edge.from.eventId
return true
}
if ('eventId' in e.from) return true
return edge.from.itemId
? e.from && e.from.itemId !== edge.from.itemId
: isDefined(e.from.itemId) || e.from.blockId !== edge.from.blockId
)
})
}

View File

@@ -0,0 +1,22 @@
import { produce } from 'immer'
import { TEvent } from '@typebot.io/schemas'
import { SetTypebot } from '../TypebotProvider'
export type EventsActions = {
updateEvent: (
eventIndex: number,
updates: Partial<Omit<TEvent, 'id'>>
) => void
}
const eventsActions = (setTypebot: SetTypebot): EventsActions => ({
updateEvent: (eventIndex: number, updates: Partial<Omit<TEvent, 'id'>>) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
const event = typebot.events[eventIndex]
typebot.events[eventIndex] = { ...event, ...updates }
})
),
})
export { eventsActions }

View File

@@ -1,11 +1,6 @@
import { createId } from '@paralleldrive/cuid2'
import { produce } from 'immer'
import {
Group,
DraggableBlock,
DraggableBlockType,
BlockIndices,
} from '@typebot.io/schemas'
import { BlockIndices, BlockV6, GroupV6 } from '@typebot.io/schemas'
import { SetTypebot } from '../TypebotProvider'
import {
deleteGroupDraft,
@@ -19,11 +14,14 @@ export type GroupsActions = {
createGroup: (
props: Coordinates & {
id: string
block: DraggableBlock | DraggableBlockType
block: BlockV6 | BlockV6['type']
indices: BlockIndices
}
) => void
updateGroup: (groupIndex: number, updates: Partial<Omit<Group, 'id'>>) => void
updateGroup: (
groupIndex: number,
updates: Partial<Omit<GroupV6, 'id'>>
) => void
duplicateGroup: (groupIndex: number) => void
deleteGroup: (groupIndex: number) => void
}
@@ -36,22 +34,22 @@ const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({
...graphCoordinates
}: Coordinates & {
id: string
block: DraggableBlock | DraggableBlockType
block: BlockV6 | BlockV6['type']
indices: BlockIndices
}) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
const newGroup: Group = {
const newGroup: GroupV6 = {
id,
graphCoordinates,
title: `Group #${typebot.groups.length}`,
title: `Group #${typebot.groups.length + 1}`,
blocks: [],
}
typebot.groups.push(newGroup)
createBlockDraft(typebot, block, newGroup.id, indices)
createBlockDraft(typebot, block, indices)
})
),
updateGroup: (groupIndex: number, updates: Partial<Omit<Group, 'id'>>) =>
updateGroup: (groupIndex: number, updates: Partial<Omit<GroupV6, 'id'>>) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
const block = typebot.groups[groupIndex]
@@ -68,7 +66,7 @@ const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({
(g) => g.title === group.title
).length
const newGroup: Group = {
const newGroup: GroupV6 = {
...group,
title: isEmpty(group.title)
? ''
@@ -78,7 +76,7 @@ const groupsActions = (setTypebot: SetTypebot): GroupsActions => ({
: ''
}`,
id,
blocks: group.blocks.map((block) => duplicateBlockDraft(id)(block)),
blocks: group.blocks.map((block) => duplicateBlockDraft(block)),
graphCoordinates: {
x: group.graphCoordinates.x + 200,
y: group.graphCoordinates.y + 100,

View File

@@ -2,10 +2,6 @@ import {
ItemIndices,
Item,
BlockWithItems,
defaultConditionContent,
Block,
LogicBlockType,
InputBlockType,
ConditionItem,
ButtonItem,
PictureChoiceItem,
@@ -15,12 +11,14 @@ import { Draft, produce } from 'immer'
import { cleanUpEdgeDraft } from './edges'
import { byId, blockHasItems } from '@typebot.io/lib'
import { createId } from '@paralleldrive/cuid2'
import { DraggabbleItem } from '@/features/graph/providers/GraphDndProvider'
import {
BlockWithCreatableItems,
DraggableItem,
} from '@/features/graph/providers/GraphDndProvider'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/constants'
type NewItem = Pick<DraggabbleItem, 'blockId' | 'outgoingEdgeId' | 'type'> &
Partial<DraggabbleItem>
type BlockWithCreatableItems = Extract<Block, { items: DraggabbleItem[] }>
type NewItem = Pick<DraggableItem, 'outgoingEdgeId'> & Partial<DraggableItem>
export type ItemsActions = {
createItem: (item: NewItem, indices: ItemIndices) => void
@@ -41,7 +39,7 @@ const createItem = (
const newItem = {
...baseItem,
id: 'id' in item && item.id ? item.id : createId(),
content: baseItem.content ?? defaultConditionContent,
content: baseItem.content,
}
block.items.splice(itemIndex, 0, newItem)
return newItem
@@ -79,7 +77,7 @@ const duplicateItem = (
const newItem = {
...baseItem,
id: createId(),
content: baseItem.content ?? defaultConditionContent,
content: baseItem.content,
}
block.items.splice(itemIndex + 1, 0, newItem)
return newItem
@@ -125,7 +123,6 @@ const itemsAction = (setTypebot: SetTypebot): ItemsActions => ({
const edgeIndex = typebot.edges.findIndex(byId(item.outgoingEdgeId))
edgeIndex !== -1
? (typebot.edges[edgeIndex].from = {
groupId: block.groupId,
blockId: block.id,
itemId: newItem.id,
})