🐛 (editor) Fix single block duplication
This commit is contained in:
@@ -1,12 +1,7 @@
|
||||
import { Fade, Flex, FlexProps, useEventListener } from '@chakra-ui/react'
|
||||
import React, { useRef, useMemo, useEffect, useState } from 'react'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import {
|
||||
BlockV6,
|
||||
GroupV6,
|
||||
PublicTypebotV6,
|
||||
TypebotV6,
|
||||
} from '@typebot.io/schemas'
|
||||
import { BlockV6, PublicTypebotV6, TypebotV6 } from '@typebot.io/schemas'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import GraphElements from './GraphElements'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
@@ -56,7 +51,7 @@ export const Graph = ({
|
||||
draggedItem,
|
||||
setDraggedItem,
|
||||
} = useBlockDnd()
|
||||
const { pasteGroups, createGroup } = useTypebot()
|
||||
const { createGroup } = useTypebot()
|
||||
const { user } = useUser()
|
||||
const {
|
||||
isReadOnly,
|
||||
@@ -84,9 +79,6 @@ export const Graph = ({
|
||||
setFocusedGroups: state.setFocusedGroups,
|
||||
}))
|
||||
)
|
||||
const groupsInClipboard = useGroupsStore(
|
||||
useShallow((state) => state.groupsInClipboard)
|
||||
)
|
||||
|
||||
const [graphPosition, setGraphPosition] = useState(
|
||||
graphPositionDefaultValue(
|
||||
@@ -319,27 +311,7 @@ export const Graph = ({
|
||||
setAutoMoveDirection(undefined)
|
||||
}
|
||||
|
||||
useKeyboardShortcuts({
|
||||
paste: () => {
|
||||
if (!groupsInClipboard || isReadOnly) return
|
||||
const { groups, oldToNewIdsMapping } = parseGroupsToPaste(
|
||||
groupsInClipboard.groups,
|
||||
lastMouseClickPosition ??
|
||||
projectMouse(
|
||||
{
|
||||
x: window.innerWidth / 2,
|
||||
y: window.innerHeight / 2,
|
||||
},
|
||||
graphPosition
|
||||
)
|
||||
)
|
||||
groups.forEach((group) => {
|
||||
updateGroupCoordinates(group.id, group.graphCoordinates)
|
||||
})
|
||||
pasteGroups(groups, groupsInClipboard.edges, oldToNewIdsMapping)
|
||||
setFocusedGroups(groups.map((g) => g.id))
|
||||
},
|
||||
})
|
||||
useKeyboardShortcuts({})
|
||||
|
||||
useEventListener('keydown', (e) => {
|
||||
if (e.key === ' ') setIsDraggingGraph(true)
|
||||
@@ -383,8 +355,11 @@ export const Graph = ({
|
||||
{selectBoxCoordinates && <SelectBox {...selectBoxCoordinates} />}
|
||||
<Fade in={!isReadOnly && focusedGroups.length > 1}>
|
||||
<GroupSelectionMenu
|
||||
lastMouseClickPosition={lastMouseClickPosition}
|
||||
focusedGroups={focusedGroups}
|
||||
blurGroups={blurGroups}
|
||||
graphPosition={graphPosition}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
</Fade>
|
||||
</>
|
||||
@@ -453,42 +428,3 @@ const useAutoMoveBoard = (
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [autoMoveDirection, setGraphPosition])
|
||||
|
||||
const parseGroupsToPaste = (
|
||||
groups: GroupV6[],
|
||||
mousePosition: Coordinates
|
||||
): { groups: GroupV6[]; oldToNewIdsMapping: Map<string, string> } => {
|
||||
const farLeftGroup = groups.sort(
|
||||
(a, b) => a.graphCoordinates.x - b.graphCoordinates.x
|
||||
)[0]
|
||||
const farLeftGroupCoord = farLeftGroup.graphCoordinates
|
||||
|
||||
const oldToNewIdsMapping = new Map<string, string>()
|
||||
const newGroups = groups.map((group) => {
|
||||
const newId = createId()
|
||||
oldToNewIdsMapping.set(group.id, newId)
|
||||
|
||||
return {
|
||||
...group,
|
||||
id: newId,
|
||||
graphCoordinates:
|
||||
group.id === farLeftGroup.id
|
||||
? mousePosition
|
||||
: {
|
||||
x:
|
||||
mousePosition.x +
|
||||
group.graphCoordinates.x -
|
||||
farLeftGroupCoord.x,
|
||||
y:
|
||||
mousePosition.y +
|
||||
group.graphCoordinates.y -
|
||||
farLeftGroupCoord.y,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
groups: newGroups,
|
||||
oldToNewIdsMapping,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,29 +12,55 @@ import {
|
||||
import { useRef } from 'react'
|
||||
import { useGroupsStore } from '../hooks/useGroupsStore'
|
||||
import { toast } from 'sonner'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { Edge, GroupV6 } from '@typebot.io/schemas'
|
||||
import { projectMouse } from '../helpers/projectMouse'
|
||||
import { Coordinates } from '../types'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
|
||||
type Props = {
|
||||
graphPosition: Coordinates & { scale: number }
|
||||
isReadOnly: boolean
|
||||
lastMouseClickPosition: Coordinates | undefined
|
||||
focusedGroups: string[]
|
||||
blurGroups: () => void
|
||||
}
|
||||
|
||||
export const GroupSelectionMenu = ({ focusedGroups, blurGroups }: Props) => {
|
||||
const { typebot, deleteGroups } = useTypebot()
|
||||
export const GroupSelectionMenu = ({
|
||||
graphPosition,
|
||||
lastMouseClickPosition,
|
||||
isReadOnly,
|
||||
focusedGroups,
|
||||
blurGroups,
|
||||
}: Props) => {
|
||||
const { typebot, deleteGroups, pasteGroups } = useTypebot()
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const copyGroups = useGroupsStore((state) => state.copyGroups)
|
||||
|
||||
const groupsInClipboard = useGroupsStore(
|
||||
useShallow((state) => state.groupsInClipboard)
|
||||
)
|
||||
const { copyGroups, setFocusedGroups, updateGroupCoordinates } =
|
||||
useGroupsStore(
|
||||
useShallow((state) => ({
|
||||
copyGroups: state.copyGroups,
|
||||
updateGroupCoordinates: state.updateGroupCoordinates,
|
||||
setFocusedGroups: state.setFocusedGroups,
|
||||
}))
|
||||
)
|
||||
|
||||
useEventListener('pointerup', (e) => e.stopPropagation(), ref.current)
|
||||
|
||||
const handleCopy = () => {
|
||||
if (!typebot) return
|
||||
const groups = typebot.groups.filter((g) => focusedGroups.includes(g.id))
|
||||
copyGroups(
|
||||
groups,
|
||||
typebot.edges.filter((edge) =>
|
||||
groups.find((g) => g.id === edge.to.groupId)
|
||||
)
|
||||
const edges = typebot.edges.filter((edge) =>
|
||||
groups.find((g) => g.id === edge.to.groupId)
|
||||
)
|
||||
toast('Groups copied to clipboard')
|
||||
copyGroups(groups, edges)
|
||||
return {
|
||||
groups,
|
||||
edges,
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
@@ -42,13 +68,45 @@ export const GroupSelectionMenu = ({ focusedGroups, blurGroups }: Props) => {
|
||||
blurGroups()
|
||||
}
|
||||
|
||||
const handlePaste = (overrideClipBoard?: {
|
||||
groups: GroupV6[]
|
||||
edges: Edge[]
|
||||
}) => {
|
||||
if (!groupsInClipboard || isReadOnly) return
|
||||
const clipboard = overrideClipBoard ?? groupsInClipboard
|
||||
const { groups, oldToNewIdsMapping } = parseGroupsToPaste(
|
||||
clipboard.groups,
|
||||
lastMouseClickPosition ??
|
||||
projectMouse(
|
||||
{
|
||||
x: window.innerWidth / 2,
|
||||
y: window.innerHeight / 2,
|
||||
},
|
||||
graphPosition
|
||||
)
|
||||
)
|
||||
groups.forEach((group) => {
|
||||
updateGroupCoordinates(group.id, group.graphCoordinates)
|
||||
})
|
||||
pasteGroups(groups, clipboard.edges, oldToNewIdsMapping)
|
||||
setFocusedGroups(groups.map((g) => g.id))
|
||||
}
|
||||
|
||||
useKeyboardShortcuts({
|
||||
copy: handleCopy,
|
||||
copy: () => {
|
||||
handleCopy()
|
||||
toast('Groups copied to clipboard')
|
||||
},
|
||||
cut: () => {
|
||||
handleCopy()
|
||||
handleDelete()
|
||||
},
|
||||
duplicate: () => {
|
||||
const clipboard = handleCopy()
|
||||
handlePaste(clipboard)
|
||||
},
|
||||
backspace: handleDelete,
|
||||
paste: handlePaste,
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -95,3 +153,42 @@ export const GroupSelectionMenu = ({ focusedGroups, blurGroups }: Props) => {
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
const parseGroupsToPaste = (
|
||||
groups: GroupV6[],
|
||||
mousePosition: Coordinates
|
||||
): { groups: GroupV6[]; oldToNewIdsMapping: Map<string, string> } => {
|
||||
const farLeftGroup = groups.sort(
|
||||
(a, b) => a.graphCoordinates.x - b.graphCoordinates.x
|
||||
)[0]
|
||||
const farLeftGroupCoord = farLeftGroup.graphCoordinates
|
||||
|
||||
const oldToNewIdsMapping = new Map<string, string>()
|
||||
const newGroups = groups.map((group) => {
|
||||
const newId = createId()
|
||||
oldToNewIdsMapping.set(group.id, newId)
|
||||
|
||||
return {
|
||||
...group,
|
||||
id: newId,
|
||||
graphCoordinates:
|
||||
group.id === farLeftGroup.id
|
||||
? mousePosition
|
||||
: {
|
||||
x:
|
||||
mousePosition.x +
|
||||
group.graphCoordinates.x -
|
||||
farLeftGroupCoord.x,
|
||||
y:
|
||||
mousePosition.y +
|
||||
group.graphCoordinates.y -
|
||||
farLeftGroupCoord.y,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
groups: newGroups,
|
||||
oldToNewIdsMapping,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,18 +10,19 @@ import {
|
||||
type Props = {
|
||||
groupId: string
|
||||
onPlayClick: () => void
|
||||
onDuplicateClick: () => void
|
||||
onDeleteClick: () => void
|
||||
}
|
||||
|
||||
export const GroupFocusToolbar = ({
|
||||
groupId,
|
||||
onPlayClick,
|
||||
onDuplicateClick,
|
||||
onDeleteClick,
|
||||
}: Props) => {
|
||||
export const GroupFocusToolbar = ({ groupId, onPlayClick }: Props) => {
|
||||
const { hasCopied, onCopy } = useClipboard(groupId)
|
||||
|
||||
const dispatchCopyEvent = () => {
|
||||
dispatchEvent(new KeyboardEvent('keydown', { key: 'c', metaKey: true }))
|
||||
}
|
||||
|
||||
const dispatchDeleteEvent = () => {
|
||||
dispatchEvent(new KeyboardEvent('keydown', { key: 'Backspace' }))
|
||||
}
|
||||
|
||||
return (
|
||||
<HStack
|
||||
rounded="md"
|
||||
@@ -44,11 +45,11 @@ export const GroupFocusToolbar = ({
|
||||
borderRightWidth="1px"
|
||||
borderRightRadius="none"
|
||||
borderLeftRadius="none"
|
||||
aria-label={'Duplicate group'}
|
||||
aria-label={'Copy group'}
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDuplicateClick()
|
||||
dispatchCopyEvent()
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
@@ -72,7 +73,7 @@ export const GroupFocusToolbar = ({
|
||||
aria-label="Delete"
|
||||
borderLeftRadius="none"
|
||||
icon={<TrashIcon />}
|
||||
onClick={onDeleteClick}
|
||||
onClick={dispatchDeleteEvent}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
@@ -44,13 +44,7 @@ export const GroupNode = ({ group, groupIndex }: Props) => {
|
||||
isReadOnly,
|
||||
graphPosition,
|
||||
} = useGraph()
|
||||
const {
|
||||
typebot,
|
||||
updateGroup,
|
||||
updateGroupsCoordinates,
|
||||
deleteGroup,
|
||||
duplicateGroup,
|
||||
} = useTypebot()
|
||||
const { typebot, updateGroup, updateGroupsCoordinates } = useTypebot()
|
||||
const { setMouseOverGroup, mouseOverGroup } = useBlockDnd()
|
||||
const { setRightPanel, setStartPreviewAtGroup } = useEditor()
|
||||
|
||||
@@ -159,7 +153,8 @@ export const GroupNode = ({ group, groupIndex }: Props) => {
|
||||
|
||||
return (
|
||||
<ContextMenu<HTMLDivElement>
|
||||
renderMenu={() => <GroupNodeContextMenu groupIndex={groupIndex} />}
|
||||
onOpen={() => focusGroup(group.id)}
|
||||
renderMenu={() => <GroupNodeContextMenu />}
|
||||
isDisabled={isReadOnly}
|
||||
>
|
||||
{(ref, isContextMenuOpened) => (
|
||||
@@ -241,10 +236,6 @@ export const GroupNode = ({ group, groupIndex }: Props) => {
|
||||
<GroupFocusToolbar
|
||||
groupId={group.id}
|
||||
onPlayClick={startPreviewAtThisGroup}
|
||||
onDuplicateClick={() => {
|
||||
duplicateGroup(groupIndex)
|
||||
}}
|
||||
onDeleteClick={() => deleteGroup(groupIndex)}
|
||||
/>
|
||||
</SlideFade>
|
||||
)}
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
import { MenuList, MenuItem } from '@chakra-ui/react'
|
||||
import { CopyIcon, TrashIcon } from '@/components/icons'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { useTranslate } from '@tolgee/react'
|
||||
|
||||
export const GroupNodeContextMenu = ({
|
||||
groupIndex,
|
||||
}: {
|
||||
groupIndex: number
|
||||
}) => {
|
||||
export const GroupNodeContextMenu = () => {
|
||||
const { t } = useTranslate()
|
||||
const { deleteGroup, duplicateGroup } = useTypebot()
|
||||
|
||||
const handleDeleteClick = () => deleteGroup(groupIndex)
|
||||
const handleDeleteClick = () =>
|
||||
dispatchEvent(new KeyboardEvent('keydown', { key: 'Backspace' }))
|
||||
|
||||
const handleDuplicateClick = () => duplicateGroup(groupIndex)
|
||||
const handleDuplicateClick = () => {
|
||||
dispatchEvent(new KeyboardEvent('keydown', { key: 'c', metaKey: true }))
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuList>
|
||||
<MenuItem icon={<CopyIcon />} onClick={handleDuplicateClick}>
|
||||
{t('duplicate')}
|
||||
{t('copy')}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<TrashIcon />} onClick={handleDeleteClick}>
|
||||
{t('delete')}
|
||||
|
||||
Reference in New Issue
Block a user