From 655ab06b3d79f1b8f37161fa4679c279beec829e Mon Sep 17 00:00:00 2001 From: Roman Snapko Date: Thu, 20 Mar 2025 10:34:37 +0100 Subject: [PATCH 01/18] feat: add workflow copy handler --- .../components/flow-canvas/canvas-context.tsx | 36 ++++++++++++++++--- .../src/components/flow-canvas/constants.ts | 1 + 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/packages/ui-components/src/components/flow-canvas/canvas-context.tsx b/packages/ui-components/src/components/flow-canvas/canvas-context.tsx index 2507b59140..dbab439f96 100644 --- a/packages/ui-components/src/components/flow-canvas/canvas-context.tsx +++ b/packages/ui-components/src/components/flow-canvas/canvas-context.tsx @@ -10,10 +10,13 @@ import { ReactNode, useCallback, useContext, + useEffect, useMemo, + useRef, useState, } from 'react'; -import { SHIFT_KEY, SPACE_KEY } from './constants'; +import { useDebounceCallback } from 'usehooks-ts'; +import { COPY_KEYS, SHIFT_KEY, SPACE_KEY } from './constants'; export type PanningMode = 'grab' | 'pan'; @@ -27,17 +30,27 @@ type CanvasContextState = { const CanvasContext = createContext(undefined); export const CanvasContextProvider = ({ + flowCanvasContainerId, children, }: { + flowCanvasContainerId?: string; children: ReactNode; }) => { const [panningMode, setPanningMode] = useState('grab'); const [selectedActions, setSelectedActions] = useState([]); + const truncatedFlowRef = useRef(null); const state = useStoreApi().getState(); const spacePressed = useKeyPress(SPACE_KEY); const shiftPressed = useKeyPress(SHIFT_KEY); + const canvas = useMemo(() => { + return flowCanvasContainerId + ? document.getElementById(flowCanvasContainerId) + : null; + }, [flowCanvasContainerId]); + const copyPressed = useKeyPress(COPY_KEYS, { target: canvas }); + const effectivePanningMode: PanningMode = useMemo(() => { if ((spacePressed || panningMode === 'grab') && !shiftPressed) { return 'grab'; @@ -77,13 +90,13 @@ export const CanvasContextProvider = ({ if (!selectedSteps.length) return; - const truncatedFlow = flowHelper.truncateFlow( + truncatedFlowRef.current = flowHelper.truncateFlow( cloneDeep(selectedSteps[0]), selectedSteps[selectedSteps.length - 1].name, - ); + ) as Action; const selectedStepNames = flowHelper - .getAllSteps(truncatedFlow) + .getAllSteps(truncatedFlowRef.current) .map((step) => step.name); state.setNodes( @@ -96,6 +109,21 @@ export const CanvasContextProvider = ({ setSelectedActions([]); }, [selectedActions, state]); + const copy = useDebounceCallback(() => { + if (!truncatedFlowRef.current) { + return; + } + const flowString = JSON.stringify(truncatedFlowRef.current); + + navigator.clipboard.writeText(flowString); + }, 300); + + useEffect(() => { + if (copyPressed) { + copy(); + } + }, [copyPressed, copy]); + const contextValue = useMemo( () => ({ panningMode: effectivePanningMode, diff --git a/packages/ui-components/src/components/flow-canvas/constants.ts b/packages/ui-components/src/components/flow-canvas/constants.ts index 61e3f18c1c..373ee3fa36 100644 --- a/packages/ui-components/src/components/flow-canvas/constants.ts +++ b/packages/ui-components/src/components/flow-canvas/constants.ts @@ -1,5 +1,6 @@ export const SHIFT_KEY = 'Shift'; export const SPACE_KEY = 'Space'; +export const COPY_KEYS = ['Control+c', 'Control+C', 'Meta+c', 'Meta+C']; export const InitialZoom = { MIN: 0.5, MAX: 1.2, From 81ffc0ce4ac2d27c0afd33b5327859e9d7a1ac31 Mon Sep 17 00:00:00 2001 From: Roman Snapko Date: Thu, 20 Mar 2025 14:03:41 +0100 Subject: [PATCH 02/18] feat: add workflow copy toast --- .../components/flow-canvas/canvas-context.tsx | 24 +++++--- .../flow-canvas/copy-paste-toast.tsx | 55 +++++++++++++++++++ packages/ui-components/src/ui/toaster.tsx | 11 +++- packages/ui-components/src/ui/use-toast.tsx | 1 + 4 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 packages/ui-components/src/components/flow-canvas/copy-paste-toast.tsx diff --git a/packages/ui-components/src/components/flow-canvas/canvas-context.tsx b/packages/ui-components/src/components/flow-canvas/canvas-context.tsx index dbab439f96..35e5d2a93a 100644 --- a/packages/ui-components/src/components/flow-canvas/canvas-context.tsx +++ b/packages/ui-components/src/components/flow-canvas/canvas-context.tsx @@ -17,6 +17,7 @@ import { } from 'react'; import { useDebounceCallback } from 'usehooks-ts'; import { COPY_KEYS, SHIFT_KEY, SPACE_KEY } from './constants'; +import { copyPasteToast } from './copy-paste-toast'; export type PanningMode = 'grab' | 'pan'; @@ -38,7 +39,8 @@ export const CanvasContextProvider = ({ }) => { const [panningMode, setPanningMode] = useState('grab'); const [selectedActions, setSelectedActions] = useState([]); - const truncatedFlowRef = useRef(null); + const selectedFlowActionRef = useRef(null); + const selectedNodeCounterRef = useRef(0); const state = useStoreApi().getState(); const spacePressed = useKeyPress(SPACE_KEY); @@ -90,15 +92,17 @@ export const CanvasContextProvider = ({ if (!selectedSteps.length) return; - truncatedFlowRef.current = flowHelper.truncateFlow( + selectedFlowActionRef.current = flowHelper.truncateFlow( cloneDeep(selectedSteps[0]), selectedSteps[selectedSteps.length - 1].name, ) as Action; const selectedStepNames = flowHelper - .getAllSteps(truncatedFlowRef.current) + .getAllSteps(selectedFlowActionRef.current) .map((step) => step.name); + selectedNodeCounterRef.current = selectedStepNames.length; + state.setNodes( state.nodes.map((node) => ({ ...node, @@ -110,12 +114,18 @@ export const CanvasContextProvider = ({ }, [selectedActions, state]); const copy = useDebounceCallback(() => { - if (!truncatedFlowRef.current) { + if (!selectedFlowActionRef.current || !selectedNodeCounterRef.current) { return; } - const flowString = JSON.stringify(truncatedFlowRef.current); - - navigator.clipboard.writeText(flowString); + const flowString = JSON.stringify(selectedFlowActionRef.current); + + navigator.clipboard.writeText(flowString).then(() => { + copyPasteToast({ + success: true, + isCopy: true, + itemsCounter: selectedNodeCounterRef.current, + }); + }); }, 300); useEffect(() => { diff --git a/packages/ui-components/src/components/flow-canvas/copy-paste-toast.tsx b/packages/ui-components/src/components/flow-canvas/copy-paste-toast.tsx new file mode 100644 index 0000000000..1177f6802b --- /dev/null +++ b/packages/ui-components/src/components/flow-canvas/copy-paste-toast.tsx @@ -0,0 +1,55 @@ +import { t } from 'i18next'; +import { CircleCheckBig } from 'lucide-react'; +import { toast } from '../../ui/use-toast'; + +type CopyPasteToastProps = { + success: boolean; + isCopy: boolean; + itemsCounter: number; +}; + +const CopyPasteToastContent = ({ + success, + isCopy, + itemsCounter, +}: CopyPasteToastProps) => { + if (success) { + return ( + <> + + + {t( + isCopy + ? '{n} Steps copied to clipboard' + : '{n} Steps successfully pasted', + { n: itemsCounter }, + )} + + + ); + } else { + return ( + <> + + + {t( + isCopy ? 'Failed to copy {n} steps' : 'Failed to paste {n} steps', + { n: itemsCounter }, + )} + + + ); + } +}; + +export const copyPasteToast = (props: CopyPasteToastProps) => { + return toast({ + description: ( +
+ +
+ ), + closeButtonClassName: 'top-6 right-4 opacity-1 text-black dark:text-white', + duration: 7000, + }); +}; diff --git a/packages/ui-components/src/ui/toaster.tsx b/packages/ui-components/src/ui/toaster.tsx index 44cf5f65ca..fed6ac02cd 100644 --- a/packages/ui-components/src/ui/toaster.tsx +++ b/packages/ui-components/src/ui/toaster.tsx @@ -15,7 +15,14 @@ export function Toaster() { return ( - {toasts.map(function ({ id, title, description, action, ...props }) { + {toasts.map(function ({ + id, + title, + description, + action, + closeButtonClassName, + ...props + }) { return (
@@ -25,7 +32,7 @@ export function Toaster() { )}
{action} - +
); })} diff --git a/packages/ui-components/src/ui/use-toast.tsx b/packages/ui-components/src/ui/use-toast.tsx index 97addff8cd..b9703c5026 100644 --- a/packages/ui-components/src/ui/use-toast.tsx +++ b/packages/ui-components/src/ui/use-toast.tsx @@ -30,6 +30,7 @@ type ToasterUserProps = { title?: React.ReactNode; description?: React.ReactNode; action?: ToastActionElement; + closeButtonClassName?: string; }; type ToasterToast = ToastProps & ToasterUserProps; From e1c437dcf0be153975b80edccddcb47b30f25b15 Mon Sep 17 00:00:00 2001 From: Roman Snapko Date: Thu, 20 Mar 2025 14:08:51 +0100 Subject: [PATCH 03/18] feat: use copy function --- .../context-menu/canvas-context-menu-content.tsx | 10 ++++------ .../flow-canvas/context-menu/canvas-context-menu.tsx | 5 ++++- .../src/components/flow-canvas/canvas-context.tsx | 4 +++- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx index 3aa6bfb3cf..bfd7c17bb1 100644 --- a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx +++ b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx @@ -4,6 +4,7 @@ import { ClipboardPaste, ClipboardPlus, Copy } from 'lucide-react'; import { ContextMenuItem, ContextMenuType, + useCanvasContext, WorkflowNode, } from '@openops/components/ui'; import { ActionType, FlagId, flowHelper } from '@openops/shared'; @@ -39,6 +40,8 @@ export const CanvasContextMenuContent = ({ state.readonly, ]); + const { copy } = useCanvasContext(); + const disabled = selectedNodes.length === 0; const doSelectedNodesIncludeTrigger = selectedNodes.some( @@ -69,12 +72,7 @@ export const CanvasContextMenuContent = ({ return ( <> {showCopy && ( - { - // https://linear.app/openops/issue/OPS-852/add-copy-logic - }} - > + {t('Copy')} diff --git a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx index 3fd3d3f6b1..1ef2c5aa90 100644 --- a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx +++ b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx @@ -8,6 +8,7 @@ import { DropdownMenuTrigger, toast, UNSAVED_CHANGES_TOAST, + useCanvasContext, WorkflowNode, } from '@openops/components/ui'; import { FlagId, FlowOperationType } from '@openops/shared'; @@ -46,6 +47,8 @@ const CanvasContextMenu = memo( false; const applyOperationAndPushToHistory = useApplyOperationAndPushToHistory(); + const { copy } = useCanvasContext(); + const [selectStepByName, removeStepSelection, setAllowCanvasPanning] = useBuilderStateContext((state) => [ state.selectStepByName, @@ -134,7 +137,7 @@ const CanvasContextMenu = memo( { e.preventDefault(); - // https://linear.app/openops/issue/OPS-852/add-copy-logic + copy(); }} > diff --git a/packages/ui-components/src/components/flow-canvas/canvas-context.tsx b/packages/ui-components/src/components/flow-canvas/canvas-context.tsx index 35e5d2a93a..b26b4adb92 100644 --- a/packages/ui-components/src/components/flow-canvas/canvas-context.tsx +++ b/packages/ui-components/src/components/flow-canvas/canvas-context.tsx @@ -26,6 +26,7 @@ type CanvasContextState = { setPanningMode: React.Dispatch>; onSelectionChange: (ev: OnSelectionChangeParams) => void; onSelectionEnd: () => void; + copy: () => void; }; const CanvasContext = createContext(undefined); @@ -140,8 +141,9 @@ export const CanvasContextProvider = ({ setPanningMode, onSelectionChange, onSelectionEnd, + copy, }), - [effectivePanningMode, onSelectionChange, onSelectionEnd], + [effectivePanningMode, onSelectionChange, onSelectionEnd, copy], ); return ( From 5627e55b066929fa5b601307b2e7c0dd009e2e4a Mon Sep 17 00:00:00 2001 From: Roman Snapko Date: Thu, 20 Mar 2025 14:20:22 +0100 Subject: [PATCH 04/18] feat: add translations --- packages/react-ui/public/locales/en/translation.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/react-ui/public/locales/en/translation.json b/packages/react-ui/public/locales/en/translation.json index 416aff2305..f5e17067bb 100644 --- a/packages/react-ui/public/locales/en/translation.json +++ b/packages/react-ui/public/locales/en/translation.json @@ -116,6 +116,10 @@ "Paste after": "Paste after", "True": "True", "False": "False", + "{n} Steps copied to clipboard": "{n} Steps copied to clipboard", + "{n} Steps successfully pasted": "{n} Steps successfully pasted", + "Failed to copy {n} steps": "Failed to copy {n} steps", + "Failed to paste {n} steps": "Failed to paste {n} steps", "Invalid Move": "Invalid Move", "The destination location is inside the same step": "The destination location is inside the same step", "Incomplete settings": "Incomplete settings", From c7b573e2a5b7c0c728407c5a45d64b5e9aa0995e Mon Sep 17 00:00:00 2001 From: Roman Snapko Date: Thu, 20 Mar 2025 14:22:09 +0100 Subject: [PATCH 05/18] fix: error toast --- .../components/flow-canvas/canvas-context.tsx | 21 +++++++++++++------ .../flow-canvas/copy-paste-toast.tsx | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/ui-components/src/components/flow-canvas/canvas-context.tsx b/packages/ui-components/src/components/flow-canvas/canvas-context.tsx index b26b4adb92..00082a9ff0 100644 --- a/packages/ui-components/src/components/flow-canvas/canvas-context.tsx +++ b/packages/ui-components/src/components/flow-canvas/canvas-context.tsx @@ -120,13 +120,22 @@ export const CanvasContextProvider = ({ } const flowString = JSON.stringify(selectedFlowActionRef.current); - navigator.clipboard.writeText(flowString).then(() => { - copyPasteToast({ - success: true, - isCopy: true, - itemsCounter: selectedNodeCounterRef.current, + navigator.clipboard + .writeText(flowString) + .then(() => { + copyPasteToast({ + success: true, + isCopy: true, + itemsCounter: selectedNodeCounterRef.current, + }); + }) + .catch(() => { + copyPasteToast({ + success: true, + isCopy: true, + itemsCounter: selectedNodeCounterRef.current, + }); }); - }); }, 300); useEffect(() => { diff --git a/packages/ui-components/src/components/flow-canvas/copy-paste-toast.tsx b/packages/ui-components/src/components/flow-canvas/copy-paste-toast.tsx index 1177f6802b..ab2cae2a11 100644 --- a/packages/ui-components/src/components/flow-canvas/copy-paste-toast.tsx +++ b/packages/ui-components/src/components/flow-canvas/copy-paste-toast.tsx @@ -30,7 +30,7 @@ const CopyPasteToastContent = ({ } else { return ( <> - + {t( isCopy ? 'Failed to copy {n} steps' : 'Failed to paste {n} steps', From f1ac4f234f8646b2a011c3ff7bb82c69813fa860 Mon Sep 17 00:00:00 2001 From: Alexandru-Dan Pop Date: Thu, 20 Mar 2025 18:41:21 +0100 Subject: [PATCH 06/18] add paste WIP --- .../src/app/features/builder/builder-hooks.ts | 10 +- .../canvas-context-menu-content.tsx | 109 +++++++++++++++--- .../context-menu/context-menu-wrapper.tsx | 5 +- .../src/components/flow-canvas/clipboard.ts | 34 ++++++ .../components/flow-canvas/flow-canvas.tsx | 19 ++- .../ui-components/src/components/index.ts | 1 + 6 files changed, 155 insertions(+), 23 deletions(-) create mode 100644 packages/ui-components/src/components/flow-canvas/clipboard.ts diff --git a/packages/react-ui/src/app/features/builder/builder-hooks.ts b/packages/react-ui/src/app/features/builder/builder-hooks.ts index ab315a38b3..2e772d3e70 100644 --- a/packages/react-ui/src/app/features/builder/builder-hooks.ts +++ b/packages/react-ui/src/app/features/builder/builder-hooks.ts @@ -422,7 +422,8 @@ export type UndoHistoryRelevantFlowOperationRequest = Extract< | FlowOperationType.UPDATE_TRIGGER | FlowOperationType.UPDATE_ACTION | FlowOperationType.DUPLICATE_ACTION - | FlowOperationType.ADD_ACTION; + | FlowOperationType.ADD_ACTION + | FlowOperationType.PASTE_ACTIONS; } >; @@ -439,6 +440,13 @@ const updateFlowVersion = ( ) => void, ) => { const newFlowVersion = flowHelper.apply(state.flowVersion, operation); + if ( + operation.type === FlowOperationType.DELETE_ACTION && + operation.request.name === state.selectedStep + ) { + set({ selectedStep: undefined }); + } + const updateRequest = async () => { set({ saving: true }); try { diff --git a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx index bfd7c17bb1..3b4309d1cc 100644 --- a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx +++ b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx @@ -4,24 +4,39 @@ import { ClipboardPaste, ClipboardPlus, Copy } from 'lucide-react'; import { ContextMenuItem, ContextMenuType, + toast, + UNSAVED_CHANGES_TOAST, useCanvasContext, WorkflowNode, } from '@openops/components/ui'; -import { ActionType, FlagId, flowHelper } from '@openops/shared'; +import { + ActionType, + FlagId, + flowHelper, + FlowOperationType, + FlowVersion, + isNil, + StepLocationRelativeToParent, +} from '@openops/shared'; import { flagsHooks } from '@/app/common/hooks/flags-hooks'; import { useReactFlow } from '@xyflow/react'; +import { useCallback } from 'react'; import { useBuilderStateContext } from '../../builder-hooks'; +import { useApplyOperationAndPushToHistory } from '../../flow-version-undo-redo/hooks/apply-operation-and-push-to-history'; import { CanvasShortcuts, ShortcutWrapper } from './canvas-shortcuts'; import { CanvasContextMenuProps } from './context-menu-wrapper'; export const CanvasContextMenuContent = ({ contextMenuType, + actionToPaste, }: CanvasContextMenuProps) => { const showCopyPaste = flagsHooks.useFlag(FlagId.COPY_PASTE_ACTIONS_ENABLED).data || false; + const applyOperationAndPushToHistory = useApplyOperationAndPushToHistory(); + const { getNodes } = useReactFlow(); const nodes = getNodes() as WorkflowNode[]; @@ -35,10 +50,9 @@ export const CanvasContextMenuContent = ({ return acc; }, [] as string[]); - const [flowVersion, readonly] = useBuilderStateContext((state) => [ - state.flowVersion, - state.readonly, - ]); + const [flowVersion, readonly, selectedStep] = useBuilderStateContext( + (state) => [state.flowVersion, state.readonly, state.selectedStep], + ); const { copy } = useCanvasContext(); @@ -48,8 +62,7 @@ export const CanvasContextMenuContent = ({ (node: string) => node === flowVersion.trigger.name, ); - // https://linear.app/openops/issue/OPS-854/add-paste-logic - const disabledPaste = true; + const disabledPaste = isNil(actionToPaste); const firstSelectedStep = flowHelper.getStep(flowVersion, selectedNodes[0]); const showPasteAfterLastStep = !readonly && contextMenuType === ContextMenuType.CANVAS; @@ -60,15 +73,44 @@ export const CanvasContextMenuContent = ({ contextMenuType === ContextMenuType.STEP; const showPasteAfterCurrentStep = - selectedNodes.length === 1 && + (selectedNodes.length === 1 || selectedStep) && !readonly && contextMenuType === ContextMenuType.STEP; + const showPasteInConditionBranch = + contextMenuType === ContextMenuType.STEP && + firstSelectedStep?.type === ActionType.BRANCH; + const showCopy = showCopyPaste && !doSelectedNodesIncludeTrigger && contextMenuType === ContextMenuType.STEP; + const onPaste = useCallback( + ( + stepLocationRelativeToParent: StepLocationRelativeToParent, + selectedStep: string | null, + branchNodeId?: string, + ) => { + if (isNil(actionToPaste)) { + return; + } + applyOperationAndPushToHistory( + { + type: FlowOperationType.PASTE_ACTIONS, + request: { + action: actionToPaste, + parentStep: getParentStepForPaste(flowVersion, selectedStep), + stepLocationRelativeToParent, + branchNodeId, + }, + }, + () => toast(UNSAVED_CHANGES_TOAST), + ); + }, + [actionToPaste, flowVersion], + ); + return ( <> {showCopy && ( @@ -80,12 +122,12 @@ export const CanvasContextMenuContent = ({ )} <> - {showPasteAfterLastStep && showCopyPaste && ( + {showPasteAfterLastStep && ( { - // // https://linear.app/openops/issue/OPS-854/add-paste-logic - }} + onClick={() => + onPaste(StepLocationRelativeToParent.AFTER, selectedStep) + } className="flex items-center gap-2" > {' '} @@ -95,21 +137,36 @@ export const CanvasContextMenuContent = ({ {showPasteAsFirstLoopAction && ( { - // https://linear.app/openops/issue/OPS-854/add-paste-logic - }} + onClick={() => + onPaste(StepLocationRelativeToParent.INSIDE_LOOP, selectedStep) + } className="flex items-center gap-2" > {' '} - {t('Paste Inside Loop')} + {t('Paste inside Loop')} + + )} + {showPasteInConditionBranch && ( + + onPaste( + StepLocationRelativeToParent.INSIDE_TRUE_BRANCH, + selectedStep, + ) + } + className="flex items-center gap-2" + > + {' '} + {t('Paste inside first Branch')} )} {showPasteAfterCurrentStep && ( { - // https://linear.app/openops/issue/OPS-854/add-paste-logic - }} + onClick={() => + onPaste(StepLocationRelativeToParent.AFTER, selectedStep) + } className="flex items-center gap-2" > {' '} @@ -120,3 +177,17 @@ export const CanvasContextMenuContent = ({ ); }; + +const getParentStepForPaste = ( + flowVersion: FlowVersion, + selectedStep: string | null, +) => { + if (selectedStep) { + return selectedStep; + } + + const allSteps = flowHelper.getAllSteps(flowVersion.trigger); + const lastStep = allSteps[allSteps.length - 1]; + + return lastStep.name; +}; diff --git a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/context-menu-wrapper.tsx b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/context-menu-wrapper.tsx index 7ee36bf16c..a46aae748a 100644 --- a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/context-menu-wrapper.tsx +++ b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/context-menu-wrapper.tsx @@ -6,17 +6,19 @@ import { } from '@openops/components/ui'; import { flagsHooks } from '@/app/common/hooks/flags-hooks'; -import { FlagId } from '@openops/shared'; +import { Action, FlagId } from '@openops/shared'; import { useBuilderStateContext } from '../../builder-hooks'; import { CanvasContextMenuContent } from './canvas-context-menu-content'; export type CanvasContextMenuProps = { contextMenuType: ContextMenuType; + actionToPaste: Action | null; children?: React.ReactNode; }; const CanvasContextMenuWrapper = ({ children, + actionToPaste, contextMenuType, }: CanvasContextMenuProps) => { const readonly = useBuilderStateContext((state) => state.readonly); @@ -34,6 +36,7 @@ const CanvasContextMenuWrapper = ({ diff --git a/packages/ui-components/src/components/flow-canvas/clipboard.ts b/packages/ui-components/src/components/flow-canvas/clipboard.ts new file mode 100644 index 0000000000..a7644e9ece --- /dev/null +++ b/packages/ui-components/src/components/flow-canvas/clipboard.ts @@ -0,0 +1,34 @@ +import { Action } from '@openops/shared'; +import { useState } from 'react'; + +export async function getActionsInClipboard(): Promise { + try { + const clipboardText = await navigator.clipboard.readText(); + const request = JSON.parse(clipboardText); + + if (request && request.name && request.settings) { + return request; + } + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error getting actions in clipboard', error); + return null; + } + + return null; +} + +export const usePasteActionsInClipboard = () => { + const [actionToPaste, setActionToPaste] = useState(null); + const fetchClipboardOperations = async () => { + if (document.hasFocus()) { + const clipboardAction = await getActionsInClipboard(); + if (clipboardAction) { + setActionToPaste(clipboardAction); + } else { + setActionToPaste(null); + } + } + }; + return { actionToPaste, fetchClipboardOperations }; +}; diff --git a/packages/ui-components/src/components/flow-canvas/flow-canvas.tsx b/packages/ui-components/src/components/flow-canvas/flow-canvas.tsx index 50a0e97380..ff452afdfc 100644 --- a/packages/ui-components/src/components/flow-canvas/flow-canvas.tsx +++ b/packages/ui-components/src/components/flow-canvas/flow-canvas.tsx @@ -1,3 +1,4 @@ +import { Action } from '@openops/shared'; import { Background, EdgeTypes, @@ -8,8 +9,10 @@ import { } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import React, { ReactNode, useCallback, useRef, useState } from 'react'; +import { useEffectOnce } from 'react-use'; import { Edge, Graph, WorkflowNode } from '../../lib/flow-canvas-utils'; import { useCanvasContext } from './canvas-context'; +import { usePasteActionsInClipboard } from './clipboard'; import { InitialZoom, MAX_ZOOM, @@ -30,6 +33,7 @@ type FlowCanvasProps = { children?: ReactNode; ContextMenu?: React.ComponentType<{ contextMenuType: ContextMenuType; + actionToPaste: Action | null; children: ReactNode; }>; }; @@ -56,8 +60,14 @@ const FlowCanvas = React.memo( const [contextMenuType, setContextMenuType] = useState( ContextMenuType.CANVAS, ); + const { actionToPaste, fetchClipboardOperations } = + usePasteActionsInClipboard(); useResizeCanvas(containerRef); + useEffectOnce(() => { + fetchClipboardOperations(); + }); + const onInit = useCallback( (reactFlow: ReactFlowInstance) => { reactFlow.fitView({ @@ -79,7 +89,9 @@ const FlowCanvas = React.memo( const panOnDrag = getPanOnDrag(allowCanvasPanning, inGrabPanningMode); - const onContextMenu = (ev: React.MouseEvent) => { + const onContextMenu = async (ev: React.MouseEvent) => { + await fetchClipboardOperations(); + if (ev.target instanceof HTMLElement || ev.target instanceof SVGElement) { const stepElement = ev.target.closest( `[data-${STEP_CONTEXT_MENU_ATTRIBUTE}]`, @@ -117,7 +129,10 @@ const FlowCanvas = React.memo( return (
{!!graph && ( - + Date: Thu, 20 Mar 2025 19:05:22 +0100 Subject: [PATCH 07/18] add context menu for split --- .../canvas-context-menu-content.tsx | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx index 3b4309d1cc..9617dcd5b5 100644 --- a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx +++ b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx @@ -81,6 +81,10 @@ export const CanvasContextMenuContent = ({ contextMenuType === ContextMenuType.STEP && firstSelectedStep?.type === ActionType.BRANCH; + const showPasteInSplitBranch = + contextMenuType === ContextMenuType.STEP && + firstSelectedStep?.type === ActionType.SPLIT; + const showCopy = showCopyPaste && !doSelectedNodesIncludeTrigger && @@ -130,7 +134,7 @@ export const CanvasContextMenuContent = ({ } className="flex items-center gap-2" > - {' '} + {t('Paste After Last Step')} )} @@ -142,7 +146,7 @@ export const CanvasContextMenuContent = ({ } className="flex items-center gap-2" > - {' '} + {t('Paste inside Loop')} )} @@ -157,8 +161,25 @@ export const CanvasContextMenuContent = ({ } className="flex items-center gap-2" > - {' '} - {t('Paste inside first Branch')} + + {t('Paste inside first branch')} + + )} + {showPasteInSplitBranch && ( + { + const branchNodeId = firstSelectedStep?.settings.options[0].id; + return onPaste( + StepLocationRelativeToParent.INSIDE_SPLIT, + selectedStep, + branchNodeId, + ); + }} + className="flex items-center gap-2" + > + + {t('Paste inside default branch')} )} {showPasteAfterCurrentStep && ( @@ -169,7 +190,7 @@ export const CanvasContextMenuContent = ({ } className="flex items-center gap-2" > - {' '} + {t('Paste After')} )} From dc988b11de8d2bbd03f49e67408e1d0ac82f187e Mon Sep 17 00:00:00 2001 From: Alexandru-Dan Pop Date: Mon, 24 Mar 2025 11:35:38 +0200 Subject: [PATCH 08/18] extract use paste hook --- .../canvas-context-menu-content.tsx | 69 ++++++------------- .../app/features/builder/hooks/use-paste.ts | 61 ++++++++++++++++ 2 files changed, 81 insertions(+), 49 deletions(-) create mode 100644 packages/react-ui/src/app/features/builder/hooks/use-paste.ts diff --git a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx index 9617dcd5b5..36bda147ac 100644 --- a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx +++ b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx @@ -4,26 +4,22 @@ import { ClipboardPaste, ClipboardPlus, Copy } from 'lucide-react'; import { ContextMenuItem, ContextMenuType, - toast, - UNSAVED_CHANGES_TOAST, useCanvasContext, WorkflowNode, } from '@openops/components/ui'; import { + Action, ActionType, FlagId, flowHelper, - FlowOperationType, - FlowVersion, isNil, StepLocationRelativeToParent, } from '@openops/shared'; import { flagsHooks } from '@/app/common/hooks/flags-hooks'; import { useReactFlow } from '@xyflow/react'; -import { useCallback } from 'react'; import { useBuilderStateContext } from '../../builder-hooks'; -import { useApplyOperationAndPushToHistory } from '../../flow-version-undo-redo/hooks/apply-operation-and-push-to-history'; +import { usePaste } from '../../hooks/use-paste'; import { CanvasShortcuts, ShortcutWrapper } from './canvas-shortcuts'; import { CanvasContextMenuProps } from './context-menu-wrapper'; @@ -35,8 +31,6 @@ export const CanvasContextMenuContent = ({ flagsHooks.useFlag(FlagId.COPY_PASTE_ACTIONS_ENABLED).data || false; - const applyOperationAndPushToHistory = useApplyOperationAndPushToHistory(); - const { getNodes } = useReactFlow(); const nodes = getNodes() as WorkflowNode[]; @@ -90,30 +84,7 @@ export const CanvasContextMenuContent = ({ !doSelectedNodesIncludeTrigger && contextMenuType === ContextMenuType.STEP; - const onPaste = useCallback( - ( - stepLocationRelativeToParent: StepLocationRelativeToParent, - selectedStep: string | null, - branchNodeId?: string, - ) => { - if (isNil(actionToPaste)) { - return; - } - applyOperationAndPushToHistory( - { - type: FlowOperationType.PASTE_ACTIONS, - request: { - action: actionToPaste, - parentStep: getParentStepForPaste(flowVersion, selectedStep), - stepLocationRelativeToParent, - branchNodeId, - }, - }, - () => toast(UNSAVED_CHANGES_TOAST), - ); - }, - [actionToPaste, flowVersion], - ); + const { onPaste } = usePaste(); return ( <> @@ -130,7 +101,11 @@ export const CanvasContextMenuContent = ({ - onPaste(StepLocationRelativeToParent.AFTER, selectedStep) + onPaste( + actionToPaste as Action, + StepLocationRelativeToParent.AFTER, + selectedStep, + ) } className="flex items-center gap-2" > @@ -142,7 +117,11 @@ export const CanvasContextMenuContent = ({ - onPaste(StepLocationRelativeToParent.INSIDE_LOOP, selectedStep) + onPaste( + actionToPaste as Action, + StepLocationRelativeToParent.INSIDE_LOOP, + selectedStep, + ) } className="flex items-center gap-2" > @@ -155,6 +134,7 @@ export const CanvasContextMenuContent = ({ disabled={disabledPaste} onClick={() => onPaste( + actionToPaste as Action, StepLocationRelativeToParent.INSIDE_TRUE_BRANCH, selectedStep, ) @@ -171,6 +151,7 @@ export const CanvasContextMenuContent = ({ onClick={() => { const branchNodeId = firstSelectedStep?.settings.options[0].id; return onPaste( + actionToPaste as Action, StepLocationRelativeToParent.INSIDE_SPLIT, selectedStep, branchNodeId, @@ -186,7 +167,11 @@ export const CanvasContextMenuContent = ({ - onPaste(StepLocationRelativeToParent.AFTER, selectedStep) + onPaste( + actionToPaste as Action, + StepLocationRelativeToParent.AFTER, + selectedStep, + ) } className="flex items-center gap-2" > @@ -198,17 +183,3 @@ export const CanvasContextMenuContent = ({ ); }; - -const getParentStepForPaste = ( - flowVersion: FlowVersion, - selectedStep: string | null, -) => { - if (selectedStep) { - return selectedStep; - } - - const allSteps = flowHelper.getAllSteps(flowVersion.trigger); - const lastStep = allSteps[allSteps.length - 1]; - - return lastStep.name; -}; diff --git a/packages/react-ui/src/app/features/builder/hooks/use-paste.ts b/packages/react-ui/src/app/features/builder/hooks/use-paste.ts new file mode 100644 index 0000000000..4a7fa366d5 --- /dev/null +++ b/packages/react-ui/src/app/features/builder/hooks/use-paste.ts @@ -0,0 +1,61 @@ +import { toast, UNSAVED_CHANGES_TOAST } from '@openops/components/ui'; +import { + Action, + flowHelper, + FlowOperationType, + FlowVersion, + isNil, + StepLocationRelativeToParent, +} from '@openops/shared'; +import { useCallback } from 'react'; +import { useBuilderStateContext } from '../builder-hooks'; +import { useApplyOperationAndPushToHistory } from '../flow-version-undo-redo/hooks/apply-operation-and-push-to-history'; + +export const usePaste = () => { + const applyOperationAndPushToHistory = useApplyOperationAndPushToHistory(); + const flowVersion = useBuilderStateContext((state) => state.flowVersion); + + const onPaste = useCallback( + ( + actionToPaste: Action, + stepLocationRelativeToParent: StepLocationRelativeToParent, + selectedStep: string | null, + branchNodeId?: string, + ) => { + if (isNil(actionToPaste)) { + return; + } + applyOperationAndPushToHistory( + { + type: FlowOperationType.PASTE_ACTIONS, + request: { + action: actionToPaste, + parentStep: getParentStepForPaste(flowVersion, selectedStep), + stepLocationRelativeToParent, + branchNodeId, + }, + }, + () => toast(UNSAVED_CHANGES_TOAST), + ); + }, + [applyOperationAndPushToHistory, flowVersion], + ); + + return { + onPaste, + }; +}; + +const getParentStepForPaste = ( + flowVersion: FlowVersion, + selectedStep: string | null, +) => { + if (selectedStep) { + return selectedStep; + } + + const allSteps = flowHelper.getAllSteps(flowVersion.trigger); + const lastStep = allSteps[allSteps.length - 1]; + + return lastStep.name; +}; From 77c13e4fbcfd04f5743ea0b01edb3d7776c452b8 Mon Sep 17 00:00:00 2001 From: Alexandru-Dan Pop Date: Mon, 24 Mar 2025 12:08:33 +0200 Subject: [PATCH 09/18] fix copy in context menu --- .../context-menu/canvas-context-menu-content.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx index d88a499878..e6d3042747 100644 --- a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx +++ b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx @@ -48,7 +48,7 @@ export const CanvasContextMenuContent = ({ (state) => [state.flowVersion, state.readonly, state.selectedStep], ); - const { copySelectedArea } = useCanvasContext(); + const { copySelectedArea, copyAction } = useCanvasContext(); const disabled = selectedNodes.length === 0; @@ -89,7 +89,18 @@ export const CanvasContextMenuContent = ({ return ( <> {showCopy && ( - + { + if (selectedStep) { + const step = flowHelper.getStep(flowVersion, selectedStep); + copyAction(step as Action); + return; + } + + copySelectedArea(); + }} + > {t('Copy')} From 76e6cb42300e08928d8a7f405bd5cc3103fff939 Mon Sep 17 00:00:00 2001 From: Alexandru-Dan Pop Date: Mon, 24 Mar 2025 12:44:21 +0200 Subject: [PATCH 10/18] add paste logic to regular context menu --- .../context-menu/canvas-context-menu.tsx | 105 +++++++++++++++++- 1 file changed, 102 insertions(+), 3 deletions(-) diff --git a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx index 03c3691f98..b957ad885d 100644 --- a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx +++ b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx @@ -9,9 +9,16 @@ import { toast, UNSAVED_CHANGES_TOAST, useCanvasContext, + usePasteActionsInClipboard, WorkflowNode, } from '@openops/components/ui'; -import { Action, FlagId, FlowOperationType } from '@openops/shared'; +import { + Action, + ActionType, + FlagId, + FlowOperationType, + StepLocationRelativeToParent, +} from '@openops/shared'; import { t } from 'i18next'; import { @@ -24,6 +31,7 @@ import { import { memo } from 'react'; import { useBuilderStateContext } from '../../builder-hooks'; import { useApplyOperationAndPushToHistory } from '../../flow-version-undo-redo/hooks/apply-operation-and-push-to-history'; +import { usePaste } from '../../hooks/use-paste'; import { StepActionWrapper } from '../nodes/step-action-wrapper'; type Props = { @@ -48,6 +56,9 @@ const CanvasContextMenu = memo( const applyOperationAndPushToHistory = useApplyOperationAndPushToHistory(); const { copyAction } = useCanvasContext(); + const { onPaste } = usePaste(); + const { actionToPaste, fetchClipboardOperations } = + usePasteActionsInClipboard(); const [selectStepByName, removeStepSelection, setAllowCanvasPanning] = useBuilderStateContext((state) => [ @@ -92,6 +103,10 @@ const CanvasContextMenu = memo( open={openStepActionsMenu} onOpenChange={(open) => { setOpenStepActionsMenu(open); + fetchClipboardOperations(); + if (open && data.step) { + selectStepByName(data.step.name); + } }} modal={true} > @@ -163,13 +178,19 @@ const CanvasContextMenu = memo( )} - {isAction && showCopyPaste && ( + {isAction && showCopyPaste && actionToPaste && ( <> { e.preventDefault(); - // https://linear.app/openops/issue/OPS-854/add-paste-logic + if (data.step) { + onPaste( + actionToPaste as Action, + StepLocationRelativeToParent.AFTER, + data.step.name, + ); + } }} > @@ -180,6 +201,84 @@ const CanvasContextMenu = memo( )} + {isAction && + showCopyPaste && + actionToPaste && + data.step?.type === ActionType.LOOP_ON_ITEMS && ( + <> + { + e.preventDefault(); + if (data.step) { + onPaste( + actionToPaste as Action, + StepLocationRelativeToParent.INSIDE_LOOP, + data.step.name, + ); + } + }} + > + + + {t('Paste inside Loop')} + + + + )} + + {isAction && + showCopyPaste && + actionToPaste && + data.step?.type === ActionType.BRANCH && ( + <> + { + e.preventDefault(); + if (data.step) { + onPaste( + actionToPaste as Action, + StepLocationRelativeToParent.INSIDE_TRUE_BRANCH, + data.step.name, + ); + } + }} + > + + + {t('Paste inside first branch')} + + + + )} + + {isAction && + showCopyPaste && + actionToPaste && + data.step?.type === ActionType.SPLIT && ( + <> + { + e.preventDefault(); + if (data.step) { + console.log('data.step', data.step); + const branchNodeId = data.step.settings.options[0].id; + onPaste( + actionToPaste as Action, + StepLocationRelativeToParent.INSIDE_SPLIT, + data.step.name, + branchNodeId, + ); + } + }} + > + + + {t('Paste inside default branch')} + + + + )} + {isAction && ( <> From 67828e6e443dd5fab5ce038881b9741280b8532a Mon Sep 17 00:00:00 2001 From: Alexandru-Dan Pop Date: Mon, 24 Mar 2025 13:05:41 +0200 Subject: [PATCH 11/18] consistent context menu plecements --- .../context-menu/canvas-context-menu.tsx | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx index b957ad885d..196d180ff3 100644 --- a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx +++ b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx @@ -101,9 +101,9 @@ const CanvasContextMenu = memo( return ( { + onOpenChange={async (open) => { + await fetchClipboardOperations(); setOpenStepActionsMenu(open); - fetchClipboardOperations(); if (open && data.step) { selectStepByName(data.step.name); } @@ -178,29 +178,6 @@ const CanvasContextMenu = memo( )} - {isAction && showCopyPaste && actionToPaste && ( - <> - - { - e.preventDefault(); - if (data.step) { - onPaste( - actionToPaste as Action, - StepLocationRelativeToParent.AFTER, - data.step.name, - ); - } - }} - > - - - {t('Paste after')} - - - - )} - {isAction && showCopyPaste && actionToPaste && @@ -279,6 +256,29 @@ const CanvasContextMenu = memo( )} + {isAction && showCopyPaste && actionToPaste && ( + <> + + { + e.preventDefault(); + if (data.step) { + onPaste( + actionToPaste as Action, + StepLocationRelativeToParent.AFTER, + data.step.name, + ); + } + }} + > + + + {t('Paste after')} + + + + )} + {isAction && ( <> From b4e23a00b86dfd58ef18014a0029ea0e600658c8 Mon Sep 17 00:00:00 2001 From: Alexandru-Dan Pop Date: Mon, 24 Mar 2025 14:04:15 +0200 Subject: [PATCH 12/18] lint fix --- .../builder/flow-canvas/context-menu/canvas-context-menu.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx index 196d180ff3..489d8a783a 100644 --- a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx +++ b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx @@ -237,7 +237,6 @@ const CanvasContextMenu = memo( onSelect={(e) => { e.preventDefault(); if (data.step) { - console.log('data.step', data.step); const branchNodeId = data.step.settings.options[0].id; onPaste( actionToPaste as Action, From 1799e53689143aabf53bb3fb5d3df84ff52fd961 Mon Sep 17 00:00:00 2001 From: Alexandru-Dan Pop Date: Mon, 24 Mar 2025 14:07:17 +0200 Subject: [PATCH 13/18] fix sonar lint issues --- .../context-menu/canvas-context-menu.tsx | 113 +++++++++--------- .../src/components/flow-canvas/clipboard.ts | 2 +- 2 files changed, 55 insertions(+), 60 deletions(-) diff --git a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx index 489d8a783a..5bffc2d5e6 100644 --- a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx +++ b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx @@ -182,81 +182,76 @@ const CanvasContextMenu = memo( showCopyPaste && actionToPaste && data.step?.type === ActionType.LOOP_ON_ITEMS && ( - <> - { - e.preventDefault(); - if (data.step) { - onPaste( - actionToPaste as Action, - StepLocationRelativeToParent.INSIDE_LOOP, - data.step.name, - ); - } - }} - > - - - {t('Paste inside Loop')} - - - + { + e.preventDefault(); + if (data.step) { + onPaste( + actionToPaste as Action, + StepLocationRelativeToParent.INSIDE_LOOP, + data.step.name, + ); + } + }} + > + + + {t('Paste inside Loop')} + + )} {isAction && showCopyPaste && actionToPaste && data.step?.type === ActionType.BRANCH && ( - <> - { - e.preventDefault(); - if (data.step) { - onPaste( - actionToPaste as Action, - StepLocationRelativeToParent.INSIDE_TRUE_BRANCH, - data.step.name, - ); - } - }} - > - - - {t('Paste inside first branch')} - - - + { + e.preventDefault(); + if (data.step) { + onPaste( + actionToPaste as Action, + StepLocationRelativeToParent.INSIDE_TRUE_BRANCH, + data.step.name, + ); + } + }} + > + + + {t('Paste inside first branch')} + + )} {isAction && showCopyPaste && actionToPaste && data.step?.type === ActionType.SPLIT && ( - <> - { - e.preventDefault(); - if (data.step) { - const branchNodeId = data.step.settings.options[0].id; - onPaste( - actionToPaste as Action, - StepLocationRelativeToParent.INSIDE_SPLIT, - data.step.name, - branchNodeId, - ); - } - }} - > - - - {t('Paste inside default branch')} - - - + { + e.preventDefault(); + if (data.step) { + const branchNodeId = data.step.settings.options[0].id; + onPaste( + actionToPaste as Action, + StepLocationRelativeToParent.INSIDE_SPLIT, + data.step.name, + branchNodeId, + ); + } + }} + > + + + {t('Paste inside default branch')} + + )} {isAction && showCopyPaste && actionToPaste && ( <> + { diff --git a/packages/ui-components/src/components/flow-canvas/clipboard.ts b/packages/ui-components/src/components/flow-canvas/clipboard.ts index a7644e9ece..a2948b9180 100644 --- a/packages/ui-components/src/components/flow-canvas/clipboard.ts +++ b/packages/ui-components/src/components/flow-canvas/clipboard.ts @@ -6,7 +6,7 @@ export async function getActionsInClipboard(): Promise { const clipboardText = await navigator.clipboard.readText(); const request = JSON.parse(clipboardText); - if (request && request.name && request.settings) { + if (request?.name && request?.settings) { return request; } } catch (error) { From 69cd2294c4658aff24dbd9cf23c9fa880bdf3495 Mon Sep 17 00:00:00 2001 From: Alexandru-Dan Pop Date: Mon, 24 Mar 2025 15:06:57 +0200 Subject: [PATCH 14/18] fix lint --- .../builder/flow-canvas/context-menu/canvas-context-menu.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx index 5bffc2d5e6..3650ed8560 100644 --- a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx +++ b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu.tsx @@ -251,7 +251,6 @@ const CanvasContextMenu = memo( {isAction && showCopyPaste && actionToPaste && ( <> - { From cc4a19f305894e9f302e5516219f93a21dc98e68 Mon Sep 17 00:00:00 2001 From: Alexandru-Dan Pop Date: Mon, 24 Mar 2025 16:45:03 +0200 Subject: [PATCH 15/18] select context menu node --- .../flow-canvas/flow-builder-canvas.tsx | 51 +++++++++++++------ .../components/flow-canvas/flow-canvas.tsx | 7 ++- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/packages/react-ui/src/app/features/builder/flow-canvas/flow-builder-canvas.tsx b/packages/react-ui/src/app/features/builder/flow-canvas/flow-builder-canvas.tsx index b878363a81..eee189025d 100644 --- a/packages/react-ui/src/app/features/builder/flow-canvas/flow-builder-canvas.tsx +++ b/packages/react-ui/src/app/features/builder/flow-canvas/flow-builder-canvas.tsx @@ -1,5 +1,5 @@ import { getNodesBounds, useReactFlow } from '@xyflow/react'; -import React from 'react'; +import React, { useCallback } from 'react'; import { FLOW_CANVAS_Y_OFFESET } from '@/app/constants/flow-canvas'; import { @@ -10,7 +10,7 @@ import { ReturnLoopedgeButton, StepPlaceHolder, } from '@openops/components/ui'; -import { useBuilderStateContext } from '../builder-hooks'; +import { RightSideBarType, useBuilderStateContext } from '../builder-hooks'; import { CanvasContextMenuWrapper } from './context-menu/context-menu-wrapper'; import { EdgeWithButton } from './edges/edge-with-button'; import { FlowDragLayer } from './flow-drag-layer'; @@ -30,22 +30,40 @@ const nodeTypes = { }; const FlowBuilderCanvas = React.memo(() => { const { getNodes } = useReactFlow(); - const [allowCanvasPanning, graph, graphHeight] = useBuilderStateContext( - (state) => { - const previousNodes = getNodes(); - const graph = flowCanvasUtils.convertFlowVersionToGraph( - state.flowVersion, - ); - graph.nodes = graph.nodes.map((node) => { - const previousNode = previousNodes.find((n) => n.id === node.id); + const [ + allowCanvasPanning, + graph, + graphHeight, + selectStepByName, + rightSidebar, + ] = useBuilderStateContext((state) => { + const previousNodes = getNodes(); + const graph = flowCanvasUtils.convertFlowVersionToGraph(state.flowVersion); + graph.nodes = graph.nodes.map((node) => { + const previousNode = previousNodes.find((n) => n.id === node.id); - if (previousNode) { - node.selected = previousNode.selected; - } - return node; - }); - return [state.allowCanvasPanning, graph, getNodesBounds(graph.nodes)]; + if (previousNode) { + node.selected = previousNode.selected; + } + return node; + }); + return [ + state.allowCanvasPanning, + graph, + getNodesBounds(graph.nodes), + state.selectStepByName, + state.rightSidebar, + ]; + }); + + const isSidebarOpen = rightSidebar === RightSideBarType.BLOCK_SETTINGS; + const setSelectedStep = useCallback( + (stepName: string) => { + if (selectStepByName) { + selectStepByName(stepName, isSidebarOpen); + } }, + [selectStepByName, isSidebarOpen], ); return ( @@ -58,6 +76,7 @@ const FlowBuilderCanvas = React.memo(() => { topOffset={FLOW_CANVAS_Y_OFFESET} graph={graph} ContextMenu={CanvasContextMenuWrapper} + selectStepByName={setSelectedStep} > diff --git a/packages/ui-components/src/components/flow-canvas/flow-canvas.tsx b/packages/ui-components/src/components/flow-canvas/flow-canvas.tsx index ff452afdfc..74edf8771a 100644 --- a/packages/ui-components/src/components/flow-canvas/flow-canvas.tsx +++ b/packages/ui-components/src/components/flow-canvas/flow-canvas.tsx @@ -30,12 +30,13 @@ type FlowCanvasProps = { graph?: Graph; topOffset?: number; allowCanvasPanning?: boolean; - children?: ReactNode; + selectStepByName?: (stepName: string) => void; ContextMenu?: React.ComponentType<{ contextMenuType: ContextMenuType; actionToPaste: Action | null; children: ReactNode; }>; + children?: ReactNode; }; function getPanOnDrag(allowCanvasPanning: boolean, inGrabPanningMode: boolean) { @@ -52,6 +53,7 @@ const FlowCanvas = React.memo( graph, topOffset, allowCanvasPanning = true, + selectStepByName, ContextMenu = ({ children }) => children, children, }: FlowCanvasProps) => { @@ -100,7 +102,8 @@ const FlowCanvas = React.memo( `data-${STEP_CONTEXT_MENU_ATTRIBUTE}`, ); - if (stepName) { + if (stepName && typeof selectStepByName === 'function') { + selectStepByName(stepName); const reactFlowState = storeApi.getState(); reactFlowState.setNodes( reactFlowState.nodes.map((node) => ({ From c3aefd151bc047f3c776f186bb8e7fb7bc27b802 Mon Sep 17 00:00:00 2001 From: Alexandru-Dan Pop Date: Tue, 25 Mar 2025 10:51:19 +0200 Subject: [PATCH 16/18] enable flag --- packages/server/api/src/app/flags/flag.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/api/src/app/flags/flag.service.ts b/packages/server/api/src/app/flags/flag.service.ts index cff99cf224..640fbdc389 100644 --- a/packages/server/api/src/app/flags/flag.service.ts +++ b/packages/server/api/src/app/flags/flag.service.ts @@ -298,7 +298,7 @@ export const flagService = { }, { id: FlagId.COPY_PASTE_ACTIONS_ENABLED, - value: false, + value: true, created, updated, }, From d25bb321f3e21fc34e919143452342307cfe9311 Mon Sep 17 00:00:00 2001 From: Alexandru-Dan Pop Date: Tue, 25 Mar 2025 13:20:10 +0200 Subject: [PATCH 17/18] Update packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx Co-authored-by: Cezar --- .../flow-canvas/context-menu/canvas-context-menu-content.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx index e6d3042747..cb8bff54d3 100644 --- a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx +++ b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx @@ -160,7 +160,7 @@ export const CanvasContextMenuContent = ({ { - const branchNodeId = firstSelectedStep?.settings.options[0].id; + const branchNodeId = firstSelectedStep.settings.options[0].id; return onPaste( actionToPaste as Action, StepLocationRelativeToParent.INSIDE_SPLIT, From 339b63e6cc2fe97e942167aaa52c9a58abc8f0af Mon Sep 17 00:00:00 2001 From: Alexandru-Dan Pop Date: Tue, 25 Mar 2025 13:24:33 +0200 Subject: [PATCH 18/18] fix CR comments --- .../flow-canvas/context-menu/canvas-context-menu-content.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx index cb8bff54d3..14951c2c5b 100644 --- a/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx +++ b/packages/react-ui/src/app/features/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx @@ -51,6 +51,7 @@ export const CanvasContextMenuContent = ({ const { copySelectedArea, copyAction } = useCanvasContext(); const disabled = selectedNodes.length === 0; + const isSingleSelectedNode = selectedNodes.length === 1; const doSelectedNodesIncludeTrigger = selectedNodes.some( (node: string) => node === flowVersion.trigger.name, @@ -61,13 +62,13 @@ export const CanvasContextMenuContent = ({ const showPasteAfterLastStep = !readonly && contextMenuType === ContextMenuType.CANVAS; const showPasteAsFirstLoopAction = - selectedNodes.length === 1 && + isSingleSelectedNode && firstSelectedStep?.type === ActionType.LOOP_ON_ITEMS && !readonly && contextMenuType === ContextMenuType.STEP; const showPasteAfterCurrentStep = - (selectedNodes.length === 1 || selectedStep) && + (isSingleSelectedNode || selectedStep) && !readonly && contextMenuType === ContextMenuType.STEP;