2
0

feat(editor): Duplicate blocks & steps

This commit is contained in:
Baptiste Arnaud
2022-03-23 11:00:43 +01:00
parent 07042137fb
commit c01ffa3f0b
6 changed files with 62 additions and 8 deletions

View File

@ -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>
)

View File

@ -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>

View File

@ -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>

View File

@ -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) => {

View File

@ -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) =>

View File

@ -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()
}) })
}) })