From b668ac15f1c5429abd4a108ac79613ad4ccf04db Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Thu, 25 Jan 2024 12:53:33 +0100 Subject: [PATCH] :bug: (editor) Fix single block duplication --- apps/builder/src/components/ContextMenu.tsx | 2 + .../src/features/graph/components/Graph.tsx | 76 +----------- .../graph/components/GroupSelectionMenu.tsx | 117 ++++++++++++++++-- .../nodes/group/GroupFocusToolbar.tsx | 23 ++-- .../components/nodes/group/GroupNode.tsx | 15 +-- .../nodes/group/GroupNodeContextMenu.tsx | 17 ++- .../builder/src/hooks/useKeyboardShortcuts.ts | 16 +++ 7 files changed, 153 insertions(+), 113 deletions(-) diff --git a/apps/builder/src/components/ContextMenu.tsx b/apps/builder/src/components/ContextMenu.tsx index 8f563aa32..9cb4e4e61 100644 --- a/apps/builder/src/components/ContextMenu.tsx +++ b/apps/builder/src/components/ContextMenu.tsx @@ -17,6 +17,7 @@ import { } from '@chakra-ui/react' export interface ContextMenuProps { + onOpen?: () => void renderMenu: () => JSX.Element | null children: ( ref: MutableRefObject, @@ -61,6 +62,7 @@ export function ContextMenu( if (e.currentTarget === targetRef.current) { e.preventDefault() e.stopPropagation() + props.onOpen?.() setIsOpened(true) setPosition([e.pageX, e.pageY]) } else { diff --git a/apps/builder/src/features/graph/components/Graph.tsx b/apps/builder/src/features/graph/components/Graph.tsx index 2f3314b4e..46cea048a 100644 --- a/apps/builder/src/features/graph/components/Graph.tsx +++ b/apps/builder/src/features/graph/components/Graph.tsx @@ -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 && } 1}> @@ -453,42 +428,3 @@ const useAutoMoveBoard = ( clearInterval(interval) } }, [autoMoveDirection, setGraphPosition]) - -const parseGroupsToPaste = ( - groups: GroupV6[], - mousePosition: Coordinates -): { groups: GroupV6[]; oldToNewIdsMapping: Map } => { - const farLeftGroup = groups.sort( - (a, b) => a.graphCoordinates.x - b.graphCoordinates.x - )[0] - const farLeftGroupCoord = farLeftGroup.graphCoordinates - - const oldToNewIdsMapping = new Map() - 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, - } -} diff --git a/apps/builder/src/features/graph/components/GroupSelectionMenu.tsx b/apps/builder/src/features/graph/components/GroupSelectionMenu.tsx index 5d3778bd2..0582fedda 100644 --- a/apps/builder/src/features/graph/components/GroupSelectionMenu.tsx +++ b/apps/builder/src/features/graph/components/GroupSelectionMenu.tsx @@ -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(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) => { ) } + +const parseGroupsToPaste = ( + groups: GroupV6[], + mousePosition: Coordinates +): { groups: GroupV6[]; oldToNewIdsMapping: Map } => { + const farLeftGroup = groups.sort( + (a, b) => a.graphCoordinates.x - b.graphCoordinates.x + )[0] + const farLeftGroupCoord = farLeftGroup.graphCoordinates + + const oldToNewIdsMapping = new Map() + 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, + } +} diff --git a/apps/builder/src/features/graph/components/nodes/group/GroupFocusToolbar.tsx b/apps/builder/src/features/graph/components/nodes/group/GroupFocusToolbar.tsx index 55d579a88..39e41590a 100644 --- a/apps/builder/src/features/graph/components/nodes/group/GroupFocusToolbar.tsx +++ b/apps/builder/src/features/graph/components/nodes/group/GroupFocusToolbar.tsx @@ -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 ( { e.stopPropagation() - onDuplicateClick() + dispatchCopyEvent() }} size="sm" /> @@ -72,7 +73,7 @@ export const GroupFocusToolbar = ({ aria-label="Delete" borderLeftRadius="none" icon={} - onClick={onDeleteClick} + onClick={dispatchDeleteEvent} variant="ghost" size="sm" /> diff --git a/apps/builder/src/features/graph/components/nodes/group/GroupNode.tsx b/apps/builder/src/features/graph/components/nodes/group/GroupNode.tsx index 9d4a98892..20fc1147f 100644 --- a/apps/builder/src/features/graph/components/nodes/group/GroupNode.tsx +++ b/apps/builder/src/features/graph/components/nodes/group/GroupNode.tsx @@ -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 ( - renderMenu={() => } + onOpen={() => focusGroup(group.id)} + renderMenu={() => } isDisabled={isReadOnly} > {(ref, isContextMenuOpened) => ( @@ -241,10 +236,6 @@ export const GroupNode = ({ group, groupIndex }: Props) => { { - duplicateGroup(groupIndex) - }} - onDeleteClick={() => deleteGroup(groupIndex)} /> )} diff --git a/apps/builder/src/features/graph/components/nodes/group/GroupNodeContextMenu.tsx b/apps/builder/src/features/graph/components/nodes/group/GroupNodeContextMenu.tsx index 8a99f76ae..947c0528d 100644 --- a/apps/builder/src/features/graph/components/nodes/group/GroupNodeContextMenu.tsx +++ b/apps/builder/src/features/graph/components/nodes/group/GroupNodeContextMenu.tsx @@ -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 ( } onClick={handleDuplicateClick}> - {t('duplicate')} + {t('copy')} } onClick={handleDeleteClick}> {t('delete')} diff --git a/apps/builder/src/hooks/useKeyboardShortcuts.ts b/apps/builder/src/hooks/useKeyboardShortcuts.ts index 45ce13032..842855b5a 100644 --- a/apps/builder/src/hooks/useKeyboardShortcuts.ts +++ b/apps/builder/src/hooks/useKeyboardShortcuts.ts @@ -6,6 +6,7 @@ type Props = { copy?: () => void paste?: () => void cut?: () => void + duplicate?: () => void backspace?: () => void } export const useKeyboardShortcuts = ({ @@ -14,6 +15,7 @@ export const useKeyboardShortcuts = ({ copy, paste, cut, + duplicate, backspace, }: Props) => { const isUndoShortcut = (event: KeyboardEvent) => @@ -31,6 +33,9 @@ export const useKeyboardShortcuts = ({ const isCutShortcut = (event: KeyboardEvent) => (event.metaKey || event.ctrlKey) && event.key === 'x' + const isDuplicateShortcut = (event: KeyboardEvent) => + (event.metaKey || event.ctrlKey) && event.key === 'd' + const isBackspaceShortcut = (event: KeyboardEvent) => event.key === 'Backspace' @@ -44,26 +49,37 @@ export const useKeyboardShortcuts = ({ if (undo && isUndoShortcut(event)) { event.preventDefault() undo() + return } if (redo && isRedoShortcut(event)) { event.preventDefault() redo() + return } if (copy && isCopyShortcut(event)) { event.preventDefault() copy() + return } if (paste && isPasteShortcut(event)) { event.preventDefault() paste() + return } if (cut && isCutShortcut(event)) { event.preventDefault() cut() + return + } + if (duplicate && isDuplicateShortcut(event)) { + event.preventDefault() + duplicate() + return } if (backspace && isBackspaceShortcut(event)) { event.preventDefault() backspace() + return } }) }