diff --git a/apps/builder/assets/icons.tsx b/apps/builder/assets/icons.tsx
index 5ba14117a..af083da8a 100644
--- a/apps/builder/assets/icons.tsx
+++ b/apps/builder/assets/icons.tsx
@@ -387,3 +387,10 @@ export const HelpCircleIcon = (props: IconProps) => (
)
+
+export const CopyIcon = (props: IconProps) => (
+
+
+
+
+)
diff --git a/apps/builder/components/shared/Graph/Nodes/BlockNode/BlockNodeContextMenu.tsx b/apps/builder/components/shared/Graph/Nodes/BlockNode/BlockNodeContextMenu.tsx
index 997fdec71..363c754f0 100644
--- a/apps/builder/components/shared/Graph/Nodes/BlockNode/BlockNodeContextMenu.tsx
+++ b/apps/builder/components/shared/Graph/Nodes/BlockNode/BlockNodeContextMenu.tsx
@@ -1,5 +1,5 @@
import { MenuList, MenuItem } from '@chakra-ui/react'
-import { TrashIcon } from 'assets/icons'
+import { CopyIcon, TrashIcon } from 'assets/icons'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
export const BlockNodeContextMenu = ({
@@ -7,12 +7,17 @@ export const BlockNodeContextMenu = ({
}: {
blockIndex: number
}) => {
- const { deleteBlock } = useTypebot()
+ const { deleteBlock, duplicateBlock } = useTypebot()
const handleDeleteClick = () => deleteBlock(blockIndex)
+ const handleDuplicateClick = () => duplicateBlock(blockIndex)
+
return (
+ } onClick={handleDuplicateClick}>
+ Duplicate
+
} onClick={handleDeleteClick}>
Delete
diff --git a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContextMenu.tsx b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContextMenu.tsx
index 1acfbe1ff..aa8f32cc4 100644
--- a/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContextMenu.tsx
+++ b/apps/builder/components/shared/Graph/Nodes/StepNode/StepNodeContextMenu.tsx
@@ -1,16 +1,21 @@
import { MenuList, MenuItem } from '@chakra-ui/react'
-import { TrashIcon } from 'assets/icons'
+import { CopyIcon, TrashIcon } from 'assets/icons'
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
import { StepIndices } from 'models'
type Props = { indices: StepIndices }
export const StepNodeContextMenu = ({ indices }: Props) => {
- const { deleteStep } = useTypebot()
+ const { deleteStep, duplicateStep } = useTypebot()
+
+ const handleDuplicateClick = () => duplicateStep(indices)
const handleDeleteClick = () => deleteStep(indices)
return (
+ } onClick={handleDuplicateClick}>
+ Duplicate
+
} onClick={handleDeleteClick}>
Delete
diff --git a/apps/builder/contexts/TypebotContext/actions/blocks.ts b/apps/builder/contexts/TypebotContext/actions/blocks.ts
index 3bf85d4bb..e81737efd 100644
--- a/apps/builder/contexts/TypebotContext/actions/blocks.ts
+++ b/apps/builder/contexts/TypebotContext/actions/blocks.ts
@@ -1,4 +1,5 @@
import { Coordinates } from 'contexts/GraphContext'
+import cuid from 'cuid'
import { produce } from 'immer'
import { WritableDraft } from 'immer/dist/internal'
import {
@@ -21,6 +22,7 @@ export type BlocksActions = {
}
) => void
updateBlock: (blockIndex: number, updates: Partial>) => void
+ duplicateBlock: (blockIndex: number) => void
deleteBlock: (blockIndex: number) => void
}
@@ -54,7 +56,24 @@ const blocksActions = (setTypebot: SetTypebot): BlocksActions => ({
typebot.blocks[blockIndex] = { ...block, ...updates }
})
),
-
+ duplicateBlock: (blockIndex: number) =>
+ setTypebot((typebot) =>
+ produce(typebot, (typebot) => {
+ const block = typebot.blocks[blockIndex]
+ const id = cuid()
+ const newBlock: Block = {
+ ...block,
+ title: `${block.title} copy`,
+ id,
+ steps: block.steps.map((s) => ({ ...s, blockId: id })),
+ graphCoordinates: {
+ x: block.graphCoordinates.x + 200,
+ y: block.graphCoordinates.y + 100,
+ },
+ }
+ typebot.blocks.splice(blockIndex + 1, 0, newBlock)
+ })
+ ),
deleteBlock: (blockIndex: number) =>
setTypebot((typebot) =>
produce(typebot, (typebot) => {
diff --git a/apps/builder/contexts/TypebotContext/actions/steps.ts b/apps/builder/contexts/TypebotContext/actions/steps.ts
index 8240366c7..92b2d9cf6 100644
--- a/apps/builder/contexts/TypebotContext/actions/steps.ts
+++ b/apps/builder/contexts/TypebotContext/actions/steps.ts
@@ -11,6 +11,7 @@ import { WritableDraft } from 'immer/dist/types/types-external'
import { SetTypebot } from '../TypebotContext'
import produce from 'immer'
import { cleanUpEdgeDraft, deleteEdgeDraft } from './edges'
+import cuid from 'cuid'
export type StepsActions = {
createStep: (
@@ -22,6 +23,7 @@ export type StepsActions = {
indices: StepIndices,
updates: Partial>
) => void
+ duplicateStep: (indices: StepIndices) => void
detachStepFromBlock: (indices: StepIndices) => void
deleteStep: (indices: StepIndices) => void
}
@@ -47,6 +49,18 @@ const stepsAction = (setTypebot: SetTypebot): StepsActions => ({
typebot.blocks[blockIndex].steps[stepIndex] = { ...step, ...updates }
})
),
+ duplicateStep: ({ blockIndex, stepIndex }: StepIndices) =>
+ setTypebot((typebot) =>
+ produce(typebot, (typebot) => {
+ const step = typebot.blocks[blockIndex].steps[stepIndex]
+ const id = cuid()
+ const newStep: Step = {
+ ...step,
+ id,
+ }
+ typebot.blocks[blockIndex].steps.splice(stepIndex + 1, 0, newStep)
+ })
+ ),
detachStepFromBlock: (indices: StepIndices) =>
setTypebot((typebot) => produce(typebot, removeStepFromBlock(indices))),
deleteStep: ({ blockIndex, stepIndex }: StepIndices) =>
diff --git a/apps/builder/playwright/tests/editor.spec.ts b/apps/builder/playwright/tests/editor.spec.ts
index b2430178b..913ff5274 100644
--- a/apps/builder/playwright/tests/editor.spec.ts
+++ b/apps/builder/playwright/tests/editor.spec.ts
@@ -111,11 +111,15 @@ test.describe.parallel('Editor', () => {
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Block #1', { button: 'right' })
+ await page.click('text=Duplicate')
+ await expect(page.locator('text="Block #1"')).toBeVisible()
+ await expect(page.locator('text="Block #1 copy"')).toBeVisible()
+ await page.click('text="Block #1"', { button: 'right' })
await page.click('text=Delete')
- await expect(page.locator('text=Block #1')).toBeHidden()
+ await expect(page.locator('text="Block #1"')).toBeHidden()
await page.click('button[aria-label="Undo"]')
- await expect(page.locator('text=Block #1')).toBeVisible()
+ await expect(page.locator('text="Block #1"')).toBeVisible()
await page.click('button[aria-label="Redo"]')
- await expect(page.locator('text=Block #1')).toBeHidden()
+ await expect(page.locator('text="Block #1"')).toBeHidden()
})
})