feat(editor): ✨ Duplicate blocks & steps
This commit is contained in:
@ -387,3 +387,10 @@ export const HelpCircleIcon = (props: IconProps) => (
|
|||||||
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const CopyIcon = (props: IconProps) => (
|
||||||
|
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||||
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { MenuList, MenuItem } from '@chakra-ui/react'
|
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 { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||||
|
|
||||||
export const BlockNodeContextMenu = ({
|
export const BlockNodeContextMenu = ({
|
||||||
@ -7,12 +7,17 @@ export const BlockNodeContextMenu = ({
|
|||||||
}: {
|
}: {
|
||||||
blockIndex: number
|
blockIndex: number
|
||||||
}) => {
|
}) => {
|
||||||
const { deleteBlock } = useTypebot()
|
const { deleteBlock, duplicateBlock } = useTypebot()
|
||||||
|
|
||||||
const handleDeleteClick = () => deleteBlock(blockIndex)
|
const handleDeleteClick = () => deleteBlock(blockIndex)
|
||||||
|
|
||||||
|
const handleDuplicateClick = () => duplicateBlock(blockIndex)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuList>
|
<MenuList>
|
||||||
|
<MenuItem icon={<CopyIcon />} onClick={handleDuplicateClick}>
|
||||||
|
Duplicate
|
||||||
|
</MenuItem>
|
||||||
<MenuItem icon={<TrashIcon />} onClick={handleDeleteClick}>
|
<MenuItem icon={<TrashIcon />} onClick={handleDeleteClick}>
|
||||||
Delete
|
Delete
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -1,16 +1,21 @@
|
|||||||
import { MenuList, MenuItem } from '@chakra-ui/react'
|
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 { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||||
import { StepIndices } from 'models'
|
import { StepIndices } from 'models'
|
||||||
|
|
||||||
type Props = { indices: StepIndices }
|
type Props = { indices: StepIndices }
|
||||||
export const StepNodeContextMenu = ({ indices }: Props) => {
|
export const StepNodeContextMenu = ({ indices }: Props) => {
|
||||||
const { deleteStep } = useTypebot()
|
const { deleteStep, duplicateStep } = useTypebot()
|
||||||
|
|
||||||
|
const handleDuplicateClick = () => duplicateStep(indices)
|
||||||
|
|
||||||
const handleDeleteClick = () => deleteStep(indices)
|
const handleDeleteClick = () => deleteStep(indices)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuList>
|
<MenuList>
|
||||||
|
<MenuItem icon={<CopyIcon />} onClick={handleDuplicateClick}>
|
||||||
|
Duplicate
|
||||||
|
</MenuItem>
|
||||||
<MenuItem icon={<TrashIcon />} onClick={handleDeleteClick}>
|
<MenuItem icon={<TrashIcon />} onClick={handleDeleteClick}>
|
||||||
Delete
|
Delete
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Coordinates } from 'contexts/GraphContext'
|
import { Coordinates } from 'contexts/GraphContext'
|
||||||
|
import cuid from 'cuid'
|
||||||
import { produce } from 'immer'
|
import { produce } from 'immer'
|
||||||
import { WritableDraft } from 'immer/dist/internal'
|
import { WritableDraft } from 'immer/dist/internal'
|
||||||
import {
|
import {
|
||||||
@ -21,6 +22,7 @@ export type BlocksActions = {
|
|||||||
}
|
}
|
||||||
) => void
|
) => void
|
||||||
updateBlock: (blockIndex: number, updates: Partial<Omit<Block, 'id'>>) => void
|
updateBlock: (blockIndex: number, updates: Partial<Omit<Block, 'id'>>) => void
|
||||||
|
duplicateBlock: (blockIndex: number) => void
|
||||||
deleteBlock: (blockIndex: number) => void
|
deleteBlock: (blockIndex: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +56,24 @@ const blocksActions = (setTypebot: SetTypebot): BlocksActions => ({
|
|||||||
typebot.blocks[blockIndex] = { ...block, ...updates }
|
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) =>
|
deleteBlock: (blockIndex: number) =>
|
||||||
setTypebot((typebot) =>
|
setTypebot((typebot) =>
|
||||||
produce(typebot, (typebot) => {
|
produce(typebot, (typebot) => {
|
||||||
|
@ -11,6 +11,7 @@ import { WritableDraft } from 'immer/dist/types/types-external'
|
|||||||
import { SetTypebot } from '../TypebotContext'
|
import { SetTypebot } from '../TypebotContext'
|
||||||
import produce from 'immer'
|
import produce from 'immer'
|
||||||
import { cleanUpEdgeDraft, deleteEdgeDraft } from './edges'
|
import { cleanUpEdgeDraft, deleteEdgeDraft } from './edges'
|
||||||
|
import cuid from 'cuid'
|
||||||
|
|
||||||
export type StepsActions = {
|
export type StepsActions = {
|
||||||
createStep: (
|
createStep: (
|
||||||
@ -22,6 +23,7 @@ export type StepsActions = {
|
|||||||
indices: StepIndices,
|
indices: StepIndices,
|
||||||
updates: Partial<Omit<Step, 'id' | 'type'>>
|
updates: Partial<Omit<Step, 'id' | 'type'>>
|
||||||
) => void
|
) => void
|
||||||
|
duplicateStep: (indices: StepIndices) => void
|
||||||
detachStepFromBlock: (indices: StepIndices) => void
|
detachStepFromBlock: (indices: StepIndices) => void
|
||||||
deleteStep: (indices: StepIndices) => void
|
deleteStep: (indices: StepIndices) => void
|
||||||
}
|
}
|
||||||
@ -47,6 +49,18 @@ const stepsAction = (setTypebot: SetTypebot): StepsActions => ({
|
|||||||
typebot.blocks[blockIndex].steps[stepIndex] = { ...step, ...updates }
|
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) =>
|
detachStepFromBlock: (indices: StepIndices) =>
|
||||||
setTypebot((typebot) => produce(typebot, removeStepFromBlock(indices))),
|
setTypebot((typebot) => produce(typebot, removeStepFromBlock(indices))),
|
||||||
deleteStep: ({ blockIndex, stepIndex }: StepIndices) =>
|
deleteStep: ({ blockIndex, stepIndex }: StepIndices) =>
|
||||||
|
@ -111,11 +111,15 @@ test.describe.parallel('Editor', () => {
|
|||||||
|
|
||||||
await page.goto(`/typebots/${typebotId}/edit`)
|
await page.goto(`/typebots/${typebotId}/edit`)
|
||||||
await page.click('text=Block #1', { button: 'right' })
|
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 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 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 page.click('button[aria-label="Redo"]')
|
||||||
await expect(page.locator('text=Block #1')).toBeHidden()
|
await expect(page.locator('text="Block #1"')).toBeHidden()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Reference in New Issue
Block a user