From 01e79bf3398d57d68ef756e998fd5fbfdcacfb25 Mon Sep 17 00:00:00 2001 From: arausly Date: Mon, 9 Dec 2024 16:40:47 +0100 Subject: [PATCH 01/16] added copy paste to rule editors --- .../contexts/RuleEditorModelContext.ts | 3 + .../contexts/RuleEditorUiContext.tsx | 6 +- .../RuleEditor/model/RuleEditorModel.tsx | 119 ++++++++++++++++++ .../RuleEditor/view/RuleEditorCanvas.tsx | 15 +++ .../RuleEditor/view/RuleEditorToolbar.tsx | 13 +- .../shared/RuleEditor/view/RuleEditorView.tsx | 10 +- .../view/ruleNode/SelectionMenu.tsx | 28 ++++- workspace/src/locales/manual/en.json | 4 + 8 files changed, 194 insertions(+), 4 deletions(-) diff --git a/workspace/src/app/views/shared/RuleEditor/contexts/RuleEditorModelContext.ts b/workspace/src/app/views/shared/RuleEditor/contexts/RuleEditorModelContext.ts index ffdb5ec796..00807cba93 100644 --- a/workspace/src/app/views/shared/RuleEditor/contexts/RuleEditorModelContext.ts +++ b/workspace/src/app/views/shared/RuleEditor/contexts/RuleEditorModelContext.ts @@ -86,6 +86,8 @@ export interface IModelActions { deleteEdges: (edgeIds: string[]) => void; /** Copy and paste a selection of nodes. Move pasted selection by the defined offset. */ copyAndPasteNodes: (nodeIds: string[], offset?: XYPosition) => void; + /** Just copy a selection of nodes. */ + copyNodes: (nodeIds: string[], offset?: XYPosition) => void; /** Move a single node to a new position. */ moveNode: (nodeId: string, newPosition: XYPosition) => void; /** changes the size of a node to the given new dimensions */ @@ -146,6 +148,7 @@ export const RuleEditorModelContext = React.createContext false, diff --git a/workspace/src/app/views/shared/RuleEditor/contexts/RuleEditorUiContext.tsx b/workspace/src/app/views/shared/RuleEditor/contexts/RuleEditorUiContext.tsx index 091332be6d..2070ba5597 100644 --- a/workspace/src/app/views/shared/RuleEditor/contexts/RuleEditorUiContext.tsx +++ b/workspace/src/app/views/shared/RuleEditor/contexts/RuleEditorUiContext.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { OnLoadParams } from "react-flow-renderer"; +import { Elements, OnLoadParams } from "react-flow-renderer"; /** Context for all UI related properties. */ export interface RuleEditorUiContextProps { @@ -26,6 +26,8 @@ export interface RuleEditorUiContextProps { hideMinimap?: boolean; /** Defines minimun and maximum of the available zoom levels */ zoomRange?: [number, number]; + onSelection: (elements: Elements | null) => void; + selectionState: { elements: Elements | null }; } export const RuleEditorUiContext = React.createContext({ @@ -41,4 +43,6 @@ export const RuleEditorUiContext = React.createContext showRuleOnly: false, hideMinimap: false, zoomRange: [0.25, 1.5], + onSelection: () => {}, + selectionState: { elements: null }, }); diff --git a/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx b/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx index be414020e9..06e7664ded 100644 --- a/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx +++ b/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx @@ -50,6 +50,8 @@ import StickyMenuButton from "../view/components/StickyMenuButton"; import { LanguageFilterProps } from "../view/ruleNode/PathInputOperator"; import { requestRuleOperatorPluginDetails } from "@ducks/common/requests"; import useErrorHandler from "../../../../hooks/useErrorHandler"; +import { PUBLIC_URL } from "../../../../constants/path"; +import useHotKey from "../../../../views/shared/HotKeyHandler/HotKeyHandler"; type NodeDimensions = NodeContentProps["nodeDimensions"]; @@ -126,6 +128,11 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { /** react-flow related functions */ const { setCenter } = useZoomPanHelper(); + useHotKey({ + hotkey: "mod+v", + handler: async () => await pasteNodes(), + }); + const edgeType = (ruleOperatorNode?: IRuleOperatorNode) => { if (ruleOperatorNode) { switch (ruleOperatorNode.pluginType) { @@ -1219,6 +1226,117 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { }, true); }; + const pasteNodes = async () => { + try { + const text = await navigator.clipboard.readText(); // Read text from clipboard + const pasteInfo = JSON.parse(text); // Parse JSON + const [context] = window.location.pathname.split("/").slice(-2); + if (pasteInfo[context]) { + changeElementsInternal((els) => { + const nodes = pasteInfo[context].data.nodes ?? []; + const nodeIdMap = new Map(); + const newNodes: RuleEditorNode[] = []; + nodes.forEach((node) => { + const position = { x: node.position.x + 100, y: node.position.y + 100 }; + const op = fetchRuleOperatorByPluginId(node.pluginId, node.pluginType); + if (op) { + const newNode = createNodeInternal( + op, + position, + Object.fromEntries(nodeParameters.get(node.id) ?? new Map()) + ); + if (newNode) { + nodeIdMap.set(node.id, newNode.id); + newNodes.push(newNode); + } + } + }); + + const newEdges: Edge[] = []; + pasteInfo[context].data.edges.forEach((edge) => { + if (nodeIdMap.has(edge.source) && nodeIdMap.has(edge.target)) { + const newEdge = utils.createEdge( + nodeIdMap.get(edge.source)!!, + nodeIdMap.get(edge.target)!!, + edge.targetHandle!!, + edge.type ?? "step" + ); + newEdges.push(newEdge); + } + }); + + const withNodes = addAndExecuteRuleModelChangeInternal( + RuleModelChangesFactory.addNodes(newNodes), + els + ); + resetSelectedElements(); + setTimeout(() => { + unsetUserSelection(); + setSelectedElements([...newNodes, ...newEdges]); + }, 100); + return addAndExecuteRuleModelChangeInternal(RuleModelChangesFactory.addEdges(newEdges), withNodes); + }); + } + } catch (err) { + //todo handle errors + console.error("ERROR ==>", err); + } + }; + + const copyNodes = async (nodeIds: string[]) => { + //Get nodes and related edges + const nodeIdMap = new Map(nodeIds.map((id) => [id, id])); + const edges: Partial[] = []; + + const originalNodes = utils.nodesById(elements, nodeIds); + const nodes = originalNodes.map((node) => { + const ruleOperatorNode = node.data.businessData.originalRuleOperatorNode; + return { + id: node.id, + pluginId: ruleOperatorNode.pluginId, + pluginType: ruleOperatorNode.pluginType, + position: node.position, + }; + }); + + elements.forEach((elem) => { + if (utils.isEdge(elem)) { + const edge = utils.asEdge(elem)!!; + if (nodeIdMap.has(edge.source) && nodeIdMap.has(edge.target)) { + //edges worthy of copying + edges.push({ + source: edge.source, + target: edge.target, + targetHandle: edge.targetHandle, + type: edge.type ?? "step", + }); + } + } + }); + //paste to clipboard. + const [, , , project, taskType, task] = window.location.pathname.split("/"); + navigator.clipboard + .writeText( + JSON.stringify({ + [taskType]: { + data: { + nodes, + edges, + }, + metaData: { + domain: PUBLIC_URL, + project, + task, + }, + }, + }) + ) + .catch((err) => { + //todo handle errors + console.error("ERROR ==>", err); + }); + }; + /** Copy and paste nodes with a given offset. */ const copyAndPasteNodes = (nodeIds: string[], offset: XYPosition = { x: 100, y: 100 }) => { changeElementsInternal((els) => { @@ -1780,6 +1898,7 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { deleteEdges, moveNodes, fixNodeInputs, + copyNodes, }, unsavedChanges: canUndo, isValidEdge, diff --git a/workspace/src/app/views/shared/RuleEditor/view/RuleEditorCanvas.tsx b/workspace/src/app/views/shared/RuleEditor/view/RuleEditorCanvas.tsx index b97d182d20..a454eed77b 100644 --- a/workspace/src/app/views/shared/RuleEditor/view/RuleEditorCanvas.tsx +++ b/workspace/src/app/views/shared/RuleEditor/view/RuleEditorCanvas.tsx @@ -87,6 +87,17 @@ export const RuleEditorCanvas = () => { enabled: !ruleEditorUiContext.modalShown && !hotKeysDisabled, }); + useHotKey({ + hotkey: "mod+c", + handler: (e) => { + const nodeIds = selectedNodeIds(); + if (nodeIds.length > 0) { + modelContext.executeModelEditOperation.copyNodes(nodeIds); + } + }, + enabled: !hotKeysDisabled, + }); + /** Selection helper methods. */ const selectedNodeIds = (): string[] => { const selectedNodes = modelUtils.elementNodes(selectionState.elements ?? []); @@ -439,6 +450,9 @@ export const RuleEditorCanvas = () => { cloneSelection={() => { cloneNodes(nodeIds); }} + copySelection={() => { + modelContext.executeModelEditOperation.copyNodes(nodeIds); + }} /> ); }; @@ -454,6 +468,7 @@ export const RuleEditorCanvas = () => { // Track current selection const onSelectionChange = (elements: Elements | null) => { selectionState.elements = elements; + ruleEditorUiContext.onSelection(elements); }; // Triggered after the react-flow instance has been loaded diff --git a/workspace/src/app/views/shared/RuleEditor/view/RuleEditorToolbar.tsx b/workspace/src/app/views/shared/RuleEditor/view/RuleEditorToolbar.tsx index 0786e6466b..9011531a4d 100644 --- a/workspace/src/app/views/shared/RuleEditor/view/RuleEditorToolbar.tsx +++ b/workspace/src/app/views/shared/RuleEditor/view/RuleEditorToolbar.tsx @@ -21,7 +21,7 @@ import { RuleEditorEvaluationContext, RuleEditorEvaluationContextProps } from ". import { EvaluationActivityControl } from "./evaluation/EvaluationActivityControl"; import { Prompt } from "react-router"; import { RuleValidationError } from "../RuleEditor.typings"; -import { DEFAULT_NODE_HEIGHT, DEFAULT_NODE_WIDTH } from "../model/RuleEditorModel.utils"; +import utils, { DEFAULT_NODE_HEIGHT, DEFAULT_NODE_WIDTH } from "../model/RuleEditorModel.utils"; import { RuleEditorBaseModal } from "./components/RuleEditorBaseModal"; import { ReactFlowHotkeyContext } from "@eccenca/gui-elements/src/cmem/react-flow/extensions/ReactFlowHotkeyContext"; @@ -204,6 +204,17 @@ export const RuleEditorToolbar = () => { onClick={() => setShowCreateStickyModal(true)} /> + + modelContext.executeModelEditOperation.copyNodes( + utils.elementNodes(ruleEditorUiContext.selectionState.elements ?? []).map((n) => n.id) + ) + } + /> + (""); const reactFlowWrapper = React.useRef(null); const [reactFlowInstance, setReactFlowInstance] = React.useState(undefined); + // At the moment react-flow's selection logic is buggy in some places, e.g. https://github.com/wbkd/react-flow/issues/1314 + // Until fixed, we will track selections ourselves and use them where bugs exist. + const [selectionState] = React.useState<{ elements: Elements | null }>({ elements: null }); + const onSelection = React.useCallback((elements: Elements | null) => { + selectionState.elements = elements; + }, []); return ( diff --git a/workspace/src/app/views/shared/RuleEditor/view/ruleNode/SelectionMenu.tsx b/workspace/src/app/views/shared/RuleEditor/view/ruleNode/SelectionMenu.tsx index a96619af6a..6cc91e15fa 100644 --- a/workspace/src/app/views/shared/RuleEditor/view/ruleNode/SelectionMenu.tsx +++ b/workspace/src/app/views/shared/RuleEditor/view/ruleNode/SelectionMenu.tsx @@ -13,10 +13,18 @@ interface SelectionMenuProps { removeSelection: () => any; /** Clone selection. */ cloneSelection: () => any; + /** Clone selection. */ + copySelection: () => any; } /** Rule edge menu. */ -export const SelectionMenu = ({ position, onClose, removeSelection, cloneSelection }: SelectionMenuProps) => { +export const SelectionMenu = ({ + position, + onClose, + removeSelection, + cloneSelection, + copySelection, +}: SelectionMenuProps) => { const [t] = useTranslation(); return ( // FIXME: CMEM-3742: Use a generic "tools" component or rename EdgeTools @@ -57,6 +65,24 @@ export const SelectionMenu = ({ position, onClose, removeSelection, cloneSelecti > {t("RuleEditor.selection.menu.clone.label")} + ); }; diff --git a/workspace/src/locales/manual/en.json b/workspace/src/locales/manual/en.json index b1cef18a3a..3ebecea4e8 100644 --- a/workspace/src/locales/manual/en.json +++ b/workspace/src/locales/manual/en.json @@ -646,6 +646,10 @@ "clone": { "label": "Clone selected nodes", "tooltip": "Clones all selected nodes and inter-connections and inserts them as new selection. Following key combination also triggers this action: CTRL/CMD + d" + }, + "copy": { + "label": "Copy selected nodes", + "tooltip": "Copy all selected nodes and inter-connections and inserts them as new selection. Following key combination also triggers this action: CTRL/CMD + d" } } }, From 2a8b0dbc516a974b5bf2b09dbebede332d123202 Mon Sep 17 00:00:00 2001 From: darausi Date: Tue, 17 Dec 2024 13:04:55 +0100 Subject: [PATCH 02/16] switched to clipboard data --- .../RuleEditor/model/RuleEditorModel.tsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx b/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx index 06e7664ded..dde7182f3f 100644 --- a/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx +++ b/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx @@ -128,10 +128,14 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { /** react-flow related functions */ const { setCenter } = useZoomPanHelper(); - useHotKey({ - hotkey: "mod+v", - handler: async () => await pasteNodes(), - }); + React.useEffect(() => { + const handlePaste = async (e) => await pasteNodes(e); + window.addEventListener("paste", handlePaste); + + return () => { + window.removeEventListener("paste", handlePaste); + }; + }, [nodeParameters, ruleEditorContext.operatorList]); const edgeType = (ruleOperatorNode?: IRuleOperatorNode) => { if (ruleOperatorNode) { @@ -1226,10 +1230,12 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { }, true); }; - const pasteNodes = async () => { + const pasteNodes = async (e: any) => { try { - const text = await navigator.clipboard.readText(); // Read text from clipboard - const pasteInfo = JSON.parse(text); // Parse JSON + // const text = await navigator.clipboard.readText(); // Read text from clipboard + // const pasteInfo = JSON.parse(text); // Parse JSON + const clipboardData = e.clipboardData?.getData("Text"); + const pasteInfo = JSON.parse(clipboardData); // Parse JSON const [context] = window.location.pathname.split("/").slice(-2); if (pasteInfo[context]) { changeElementsInternal((els) => { @@ -1251,7 +1257,6 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { } } }); - const newEdges: Edge[] = []; pasteInfo[context].data.edges.forEach((edge) => { if (nodeIdMap.has(edge.source) && nodeIdMap.has(edge.target)) { From 71449572ef73c876a6eccc6210fcf3e83892d5e3 Mon Sep 17 00:00:00 2001 From: darausi Date: Thu, 19 Dec 2024 10:09:41 +0100 Subject: [PATCH 03/16] refactors --- .../RuleEditor/model/RuleEditorModel.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx b/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx index dde7182f3f..42e9ab8fb6 100644 --- a/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx +++ b/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx @@ -1232,11 +1232,11 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { const pasteNodes = async (e: any) => { try { - // const text = await navigator.clipboard.readText(); // Read text from clipboard - // const pasteInfo = JSON.parse(text); // Parse JSON const clipboardData = e.clipboardData?.getData("Text"); const pasteInfo = JSON.parse(clipboardData); // Parse JSON - const [context] = window.location.pathname.split("/").slice(-2); + const context = window.location.pathname.split("/").find((path) => path === "linking") + ? "linking" + : "transform"; if (pasteInfo[context]) { changeElementsInternal((els) => { const nodes = pasteInfo[context].data.nodes ?? []; @@ -1274,6 +1274,7 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { RuleModelChangesFactory.addNodes(newNodes), els ); + console.log({ newNodes, newEdges }); resetSelectedElements(); setTimeout(() => { unsetUserSelection(); @@ -1284,7 +1285,17 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { } } catch (err) { //todo handle errors - console.error("ERROR ==>", err); + console.log("Error ==>", err); + const unExpectedTokenError = /Unexpected token/.exec(err); + if (unExpectedTokenError) { + //that is, not the expected json format that contains nodes + registerError( + "RuleEditorModel.pasteCopiedNodes", + "No operator has been found in the pasted data", + err, + RULE_EDITOR_NOTIFICATION_INSTANCE + ); + } } }; From b121810d03dcbb602011a16e7b9ef1602e6acd9d Mon Sep 17 00:00:00 2001 From: arausly Date: Thu, 2 Jan 2025 14:41:17 +0100 Subject: [PATCH 04/16] added highlight to newly pasted nodes --- .../views/shared/RuleEditor/model/RuleEditorModel.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx b/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx index 42e9ab8fb6..37ce8b8e87 100644 --- a/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx +++ b/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx @@ -1253,7 +1253,16 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { ); if (newNode) { nodeIdMap.set(node.id, newNode.id); - newNodes.push(newNode); + newNodes.push({ + ...newNode, + data: { + ...newNode.data, + introductionTime: { + run: 1800, + delay: 300, + }, + }, + }); } } }); From ef97eed8536d443cb0cdeede71df397c7ef3edff Mon Sep 17 00:00:00 2001 From: arausly Date: Mon, 6 Jan 2025 17:58:01 +0100 Subject: [PATCH 05/16] added notification errors --- .../src/app/store/ducks/error/errorSlice.ts | 3 +- .../RuleEditor/model/RuleEditorModel.tsx | 47 ++++++++----------- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/workspace/src/app/store/ducks/error/errorSlice.ts b/workspace/src/app/store/ducks/error/errorSlice.ts index a858365e6f..6616525f8d 100644 --- a/workspace/src/app/store/ducks/error/errorSlice.ts +++ b/workspace/src/app/store/ducks/error/errorSlice.ts @@ -7,7 +7,7 @@ type RegisterErrorActionType = { /** The error that should be displayed. */ newError: Pick; /** An optional error notification instance ID when this error should only be shown in a specific error notification widget. */ - errorNotificationInstanceId?: string + errorNotificationInstanceId?: string; }; }; @@ -23,6 +23,7 @@ const errorSlice = createSlice({ reducers: { registerNewError(state, action: RegisterErrorActionType) { const { newError, errorNotificationInstanceId } = action.payload; + console.log("NEW ERROR -->", newError, "instanceId -->", errorNotificationInstanceId); // Remove old error from the same component action const newErrors = state.errors.filter((err) => err.id !== newError.id); // Always add new error to the end with current timestamp diff --git a/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx b/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx index 37ce8b8e87..4562c5e2fe 100644 --- a/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx +++ b/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx @@ -1245,25 +1245,24 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { nodes.forEach((node) => { const position = { x: node.position.x + 100, y: node.position.y + 100 }; const op = fetchRuleOperatorByPluginId(node.pluginId, node.pluginType); - if (op) { - const newNode = createNodeInternal( - op, - position, - Object.fromEntries(nodeParameters.get(node.id) ?? new Map()) - ); - if (newNode) { - nodeIdMap.set(node.id, newNode.id); - newNodes.push({ - ...newNode, - data: { - ...newNode.data, - introductionTime: { - run: 1800, - delay: 300, - }, + if (!op) throw new Error(`Missing plugins for operator plugin ${node.pluginId}`); + const newNode = createNodeInternal( + op, + position, + Object.fromEntries(nodeParameters.get(node.id) ?? new Map()) + ); + if (newNode) { + nodeIdMap.set(node.id, newNode.id); + newNodes.push({ + ...newNode, + data: { + ...newNode.data, + introductionTime: { + run: 1800, + delay: 300, }, - }); - } + }, + }); } }); const newEdges: Edge[] = []; @@ -1283,7 +1282,6 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { RuleModelChangesFactory.addNodes(newNodes), els ); - console.log({ newNodes, newEdges }); resetSelectedElements(); setTimeout(() => { unsetUserSelection(); @@ -1294,17 +1292,12 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { } } catch (err) { //todo handle errors - console.log("Error ==>", err); - const unExpectedTokenError = /Unexpected token/.exec(err); + const unExpectedTokenError = /Unexpected token/.exec(err?.message ?? ""); if (unExpectedTokenError) { //that is, not the expected json format that contains nodes - registerError( - "RuleEditorModel.pasteCopiedNodes", - "No operator has been found in the pasted data", - err, - RULE_EDITOR_NOTIFICATION_INSTANCE - ); + registerError("RuleEditorModel.pasteCopiedNodes", "No operator has been found in the pasted data", err); } + registerError("RuleEditorModel.pasteCopiedNodes", err?.message, err); } }; From ed275af727474075de1a124c900caab1eb945507 Mon Sep 17 00:00:00 2001 From: arausly Date: Thu, 9 Jan 2025 14:47:04 +0100 Subject: [PATCH 06/16] removed redundant logs --- workspace/src/app/store/ducks/error/errorSlice.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/workspace/src/app/store/ducks/error/errorSlice.ts b/workspace/src/app/store/ducks/error/errorSlice.ts index 6616525f8d..5dc7d5c73e 100644 --- a/workspace/src/app/store/ducks/error/errorSlice.ts +++ b/workspace/src/app/store/ducks/error/errorSlice.ts @@ -23,7 +23,6 @@ const errorSlice = createSlice({ reducers: { registerNewError(state, action: RegisterErrorActionType) { const { newError, errorNotificationInstanceId } = action.payload; - console.log("NEW ERROR -->", newError, "instanceId -->", errorNotificationInstanceId); // Remove old error from the same component action const newErrors = state.errors.filter((err) => err.id !== newError.id); // Always add new error to the end with current timestamp From 08fdabd9a1deb67f865e33f0ac2b0d7697501b06 Mon Sep 17 00:00:00 2001 From: arausly Date: Tue, 14 Jan 2025 18:07:29 +0100 Subject: [PATCH 07/16] tried copy event instead --- .../contexts/RuleEditorModelContext.ts | 2 + .../RuleEditor/model/RuleEditorModel.tsx | 58 ++++++++++++------- .../RuleEditor/view/RuleEditorCanvas.tsx | 21 +++---- 3 files changed, 49 insertions(+), 32 deletions(-) diff --git a/workspace/src/app/views/shared/RuleEditor/contexts/RuleEditorModelContext.ts b/workspace/src/app/views/shared/RuleEditor/contexts/RuleEditorModelContext.ts index 00807cba93..76c021735b 100644 --- a/workspace/src/app/views/shared/RuleEditor/contexts/RuleEditorModelContext.ts +++ b/workspace/src/app/views/shared/RuleEditor/contexts/RuleEditorModelContext.ts @@ -47,6 +47,7 @@ export interface RuleEditorModelContextProps { ruleOperatorNodes: () => IRuleOperatorNode[]; /** The ID of the rule editor canvas element. */ canvasId: string; + updateSelectedElements: (elements: Elements | null) => void; } export interface IModelActions { @@ -131,6 +132,7 @@ export const RuleEditorModelContext = React.createContext {}, executeModelEditOperation: { startChangeTransaction: NOP, addStickyNode: NOP, diff --git a/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx b/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx index 4562c5e2fe..b2c6a8c71c 100644 --- a/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx +++ b/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx @@ -52,6 +52,7 @@ import { requestRuleOperatorPluginDetails } from "@ducks/common/requests"; import useErrorHandler from "../../../../hooks/useErrorHandler"; import { PUBLIC_URL } from "../../../../constants/path"; import useHotKey from "../../../../views/shared/HotKeyHandler/HotKeyHandler"; +import { RuleEditorUiContext } from "../contexts/RuleEditorUiContext"; type NodeDimensions = NodeContentProps["nodeDimensions"]; @@ -124,18 +125,31 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { const [utils] = React.useState(ruleEditorModelUtilsFactory()); /** ID of the rule editor canvas. This is needed for the auto-layout operation. */ const canvasId = `ruleEditor-react-flow-canvas-${ruleEditorContext.instanceId}`; + /** when a node is clicked the selected nodes appears here */ + const [selectedElements, updateSelectedElements] = React.useState(null); /** react-flow related functions */ const { setCenter } = useZoomPanHelper(); React.useEffect(() => { const handlePaste = async (e) => await pasteNodes(e); + const handleCopy = async (e) => { + selectedElements && + (await copyNodes( + selectedElements.map((n) => n.id), + e + )); + e.preventDefault(); + }; + window.addEventListener("paste", handlePaste); + window.addEventListener("copy", handleCopy); return () => { window.removeEventListener("paste", handlePaste); + window.removeEventListener("copy", handleCopy); }; - }, [nodeParameters, ruleEditorContext.operatorList]); + }, [nodeParameters, ruleEditorContext.operatorList, selectedElements]); const edgeType = (ruleOperatorNode?: IRuleOperatorNode) => { if (ruleOperatorNode) { @@ -1301,7 +1315,7 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { } }; - const copyNodes = async (nodeIds: string[]) => { + const copyNodes = async (nodeIds: string[], event?: any) => { //Get nodes and related edges const nodeIdMap = new Map(nodeIds.map((id) => [id, id])); const edges: Partial[] = []; @@ -1333,26 +1347,25 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { }); //paste to clipboard. const [, , , project, taskType, task] = window.location.pathname.split("/"); - navigator.clipboard - .writeText( - JSON.stringify({ - [taskType]: { - data: { - nodes, - edges, - }, - metaData: { - domain: PUBLIC_URL, - project, - task, - }, - }, - }) - ) - .catch((err) => { - //todo handle errors - console.error("ERROR ==>", err); - }); + const data = JSON.stringify({ + [taskType]: { + data: { + nodes, + edges, + }, + metaData: { + domain: PUBLIC_URL, + project, + task, + }, + }, + }); + event + ? event.clipboardData?.setData("text/plain", data) + : navigator.clipboard.writeText(data).catch((err) => { + //todo handle errors + console.error("ERROR ==>", err); + }); }; /** Copy and paste nodes with a given offset. */ @@ -1898,6 +1911,7 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { redo, canRedo, canvasId, + updateSelectedElements, executeModelEditOperation: { startChangeTransaction, addNode, diff --git a/workspace/src/app/views/shared/RuleEditor/view/RuleEditorCanvas.tsx b/workspace/src/app/views/shared/RuleEditor/view/RuleEditorCanvas.tsx index a454eed77b..629ce1b1e9 100644 --- a/workspace/src/app/views/shared/RuleEditor/view/RuleEditorCanvas.tsx +++ b/workspace/src/app/views/shared/RuleEditor/view/RuleEditorCanvas.tsx @@ -87,16 +87,16 @@ export const RuleEditorCanvas = () => { enabled: !ruleEditorUiContext.modalShown && !hotKeysDisabled, }); - useHotKey({ - hotkey: "mod+c", - handler: (e) => { - const nodeIds = selectedNodeIds(); - if (nodeIds.length > 0) { - modelContext.executeModelEditOperation.copyNodes(nodeIds); - } - }, - enabled: !hotKeysDisabled, - }); + // useHotKey({ + // hotkey: "mod+c", + // handler: (e) => { + // const nodeIds = selectedNodeIds(); + // if (nodeIds.length > 0) { + // modelContext.executeModelEditOperation.copyNodes(nodeIds); + // } + // }, + // enabled: !hotKeysDisabled, + // }); /** Selection helper methods. */ const selectedNodeIds = (): string[] => { @@ -469,6 +469,7 @@ export const RuleEditorCanvas = () => { const onSelectionChange = (elements: Elements | null) => { selectionState.elements = elements; ruleEditorUiContext.onSelection(elements); + modelContext.updateSelectedElements(elements); }; // Triggered after the react-flow instance has been loaded From 9b4cad7123660149f95ab0f7d980b411ff235a34 Mon Sep 17 00:00:00 2001 From: arausly Date: Tue, 14 Jan 2025 23:47:20 +0100 Subject: [PATCH 08/16] corrected copy overwrite --- .../views/shared/RuleEditor/model/RuleEditorModel.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx b/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx index b2c6a8c71c..66020eec00 100644 --- a/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx +++ b/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx @@ -134,14 +134,14 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { React.useEffect(() => { const handlePaste = async (e) => await pasteNodes(e); const handleCopy = async (e) => { - selectedElements && - (await copyNodes( + if (selectedElements) { + await copyNodes( selectedElements.map((n) => n.id), e - )); - e.preventDefault(); + ); + e.preventDefault(); + } }; - window.addEventListener("paste", handlePaste); window.addEventListener("copy", handleCopy); From 8e4f1cf4c7bfecf2c0027c1bf3586c0b2541206b Mon Sep 17 00:00:00 2001 From: Andreas Schultz Date: Wed, 15 Jan 2025 11:44:04 +0100 Subject: [PATCH 09/16] Make copy nodes work in Firefox (and probably all other browsers) --- .../shared/RuleEditor/model/RuleEditorModel.tsx | 13 +++++++------ workspace/src/locales/manual/en.json | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx b/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx index 66020eec00..9ef54b306a 100644 --- a/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx +++ b/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx @@ -53,6 +53,7 @@ import useErrorHandler from "../../../../hooks/useErrorHandler"; import { PUBLIC_URL } from "../../../../constants/path"; import useHotKey from "../../../../views/shared/HotKeyHandler/HotKeyHandler"; import { RuleEditorUiContext } from "../contexts/RuleEditorUiContext"; +import {copyToClipboard} from "../../../../utils/copyToClipboard"; type NodeDimensions = NodeContentProps["nodeDimensions"]; @@ -1360,12 +1361,12 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { }, }, }); - event - ? event.clipboardData?.setData("text/plain", data) - : navigator.clipboard.writeText(data).catch((err) => { - //todo handle errors - console.error("ERROR ==>", err); - }); + if(event) { + event.clipboardData.setData("text/plain", data) + event.preventDefault() + } else { + copyToClipboard(data) + } }; /** Copy and paste nodes with a given offset. */ diff --git a/workspace/src/locales/manual/en.json b/workspace/src/locales/manual/en.json index 3ebecea4e8..fb18547639 100644 --- a/workspace/src/locales/manual/en.json +++ b/workspace/src/locales/manual/en.json @@ -649,7 +649,7 @@ }, "copy": { "label": "Copy selected nodes", - "tooltip": "Copy all selected nodes and inter-connections and inserts them as new selection. Following key combination also triggers this action: CTRL/CMD + d" + "tooltip": "Copy all selected nodes and inter-connections. Following key combination also triggers this action: CTRL/CMD + c. The copied elements can be inserted as copies via CTRL/CMD + v, this holds true also for editors in different tabs/windows/browsers." } } }, From 6b1c5a7a684d8b1a3c4134d7bfd3290c1d1b515f Mon Sep 17 00:00:00 2001 From: arausly Date: Wed, 15 Jan 2025 15:00:12 +0100 Subject: [PATCH 10/16] corrected error overwrite --- .../shared/RuleEditor/model/RuleEditorModel.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx b/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx index 9ef54b306a..c796f0c60b 100644 --- a/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx +++ b/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx @@ -53,7 +53,7 @@ import useErrorHandler from "../../../../hooks/useErrorHandler"; import { PUBLIC_URL } from "../../../../constants/path"; import useHotKey from "../../../../views/shared/HotKeyHandler/HotKeyHandler"; import { RuleEditorUiContext } from "../contexts/RuleEditorUiContext"; -import {copyToClipboard} from "../../../../utils/copyToClipboard"; +import { copyToClipboard } from "../../../../utils/copyToClipboard"; type NodeDimensions = NodeContentProps["nodeDimensions"]; @@ -1311,8 +1311,9 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { if (unExpectedTokenError) { //that is, not the expected json format that contains nodes registerError("RuleEditorModel.pasteCopiedNodes", "No operator has been found in the pasted data", err); + } else { + registerError("RuleEditorModel.pasteCopiedNodes", err?.message, err); } - registerError("RuleEditorModel.pasteCopiedNodes", err?.message, err); } }; @@ -1361,11 +1362,11 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { }, }, }); - if(event) { - event.clipboardData.setData("text/plain", data) - event.preventDefault() + if (event) { + event.clipboardData.setData("text/plain", data); + event.preventDefault(); } else { - copyToClipboard(data) + copyToClipboard(data); } }; From 0f494f0ab853f6d640e69b155dbb27f2423a6c54 Mon Sep 17 00:00:00 2001 From: arausly Date: Fri, 17 Jan 2025 15:59:05 +0100 Subject: [PATCH 11/16] fix bug with pasting nodes --- .../views/shared/RuleEditor/RuleEditor.tsx | 4 ++-- .../RuleEditor/model/RuleEditorModel.tsx | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/workspace/src/app/views/shared/RuleEditor/RuleEditor.tsx b/workspace/src/app/views/shared/RuleEditor/RuleEditor.tsx index 301f29b583..dd6c89ebfa 100644 --- a/workspace/src/app/views/shared/RuleEditor/RuleEditor.tsx +++ b/workspace/src/app/views/shared/RuleEditor/RuleEditor.tsx @@ -19,8 +19,8 @@ import utils from "./RuleEditor.utils"; import { IStickyNote } from "views/taskViews/shared/task.typings"; import { DatasetCharacteristics } from "../typings"; import { ReactFlowHotkeyContext } from "@eccenca/gui-elements/src/cmem/react-flow/extensions/ReactFlowHotkeyContext"; -import {Notification} from "@eccenca/gui-elements" -import {diErrorMessage} from "@ducks/error/typings"; +import { Notification } from "@eccenca/gui-elements"; +import { diErrorMessage } from "@ducks/error/typings"; /** Function to fetch the rule operator spec. */ export type RuleOperatorFetchFnType = ( diff --git a/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx b/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx index c796f0c60b..3d6b2a8bc9 100644 --- a/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx +++ b/workspace/src/app/views/shared/RuleEditor/model/RuleEditorModel.tsx @@ -1249,12 +1249,12 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { try { const clipboardData = e.clipboardData?.getData("Text"); const pasteInfo = JSON.parse(clipboardData); // Parse JSON - const context = window.location.pathname.split("/").find((path) => path === "linking") - ? "linking" - : "transform"; - if (pasteInfo[context]) { + const taskType = + (ruleEditorContext.editedItem as { type: string })?.type === "linking" ? "linking" : "transform"; + + if (pasteInfo[taskType]) { changeElementsInternal((els) => { - const nodes = pasteInfo[context].data.nodes ?? []; + const nodes = pasteInfo[taskType].data.nodes ?? []; const nodeIdMap = new Map(); const newNodes: RuleEditorNode[] = []; nodes.forEach((node) => { @@ -1281,7 +1281,7 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { } }); const newEdges: Edge[] = []; - pasteInfo[context].data.edges.forEach((edge) => { + pasteInfo[taskType].data.edges.forEach((edge) => { if (nodeIdMap.has(edge.source) && nodeIdMap.has(edge.target)) { const newEdge = utils.createEdge( nodeIdMap.get(edge.source)!!, @@ -1348,7 +1348,8 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { } }); //paste to clipboard. - const [, , , project, taskType, task] = window.location.pathname.split("/"); + const { projectId, editedItemId, editedItem } = ruleEditorContext; + const taskType = (editedItem as { type: string })?.type === "linking" ? "linking" : "transform"; const data = JSON.stringify({ [taskType]: { data: { @@ -1357,8 +1358,8 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { }, metaData: { domain: PUBLIC_URL, - project, - task, + project: projectId, + task: editedItemId, }, }, }); From c332a7bc2d93ebdf408a676887a468e2b3ee9d70 Mon Sep 17 00:00:00 2001 From: arausly Date: Fri, 17 Jan 2025 16:23:22 +0100 Subject: [PATCH 12/16] increased spacing between actions for selection menu --- .../views/shared/RuleEditor/view/ruleNode/SelectionMenu.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/workspace/src/app/views/shared/RuleEditor/view/ruleNode/SelectionMenu.tsx b/workspace/src/app/views/shared/RuleEditor/view/ruleNode/SelectionMenu.tsx index 6cc91e15fa..f84a4f2da9 100644 --- a/workspace/src/app/views/shared/RuleEditor/view/ruleNode/SelectionMenu.tsx +++ b/workspace/src/app/views/shared/RuleEditor/view/ruleNode/SelectionMenu.tsx @@ -1,7 +1,7 @@ import React from "react"; import { EdgeTools } from "@eccenca/gui-elements/src/extensions/react-flow"; import { XYPosition } from "react-flow-renderer/dist/types"; -import { Button } from "@eccenca/gui-elements"; +import { Button, Spacing } from "@eccenca/gui-elements"; import { useTranslation } from "react-i18next"; interface SelectionMenuProps { @@ -40,6 +40,7 @@ export const SelectionMenu = ({ openOnTargetFocus: false, }} small + hasStateDanger onClick={() => { onClose(); removeSelection(); @@ -47,6 +48,7 @@ export const SelectionMenu = ({ > {t("RuleEditor.selection.menu.delete.label")} + + - - - - + + { + onClose(); + copySelection(); + }} + /> + { + onClose(); + cloneSelection(); + }} + /> + + { + onClose(); + removeSelection(); + }} + intent="danger" + text={t("RuleEditor.selection.menu.delete.label")} + /> + ); }; diff --git a/workspace/src/locales/manual/en.json b/workspace/src/locales/manual/en.json index fb18547639..342b75fc12 100644 --- a/workspace/src/locales/manual/en.json +++ b/workspace/src/locales/manual/en.json @@ -648,7 +648,7 @@ "tooltip": "Clones all selected nodes and inter-connections and inserts them as new selection. Following key combination also triggers this action: CTRL/CMD + d" }, "copy": { - "label": "Copy selected nodes", + "label": "Copy selected nodes to clipboard", "tooltip": "Copy all selected nodes and inter-connections. Following key combination also triggers this action: CTRL/CMD + c. The copied elements can be inserted as copies via CTRL/CMD + v, this holds true also for editors in different tabs/windows/browsers." } } @@ -701,7 +701,8 @@ }, "startEvaluation": "Start evaluation", "showEvaluation": "Show evaluation examples", - "hideEvaluation": "Hide evaluation examples" + "hideEvaluation": "Hide evaluation examples", + "copiedNotificationText": "Copied {{numberOfNodes}} to clipboard" } }, "taskViews": { From 064143b505747b54d2f4c37c24fe504304c231bd Mon Sep 17 00:00:00 2001 From: Michael Haschke Date: Wed, 29 Jan 2025 15:26:40 +0100 Subject: [PATCH 14/16] fix selection menu, use correct components --- .../view/ruleNode/SelectionMenu.tsx | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/workspace/src/app/views/shared/RuleEditor/view/ruleNode/SelectionMenu.tsx b/workspace/src/app/views/shared/RuleEditor/view/ruleNode/SelectionMenu.tsx index 9068f3d74d..96507a7a47 100644 --- a/workspace/src/app/views/shared/RuleEditor/view/ruleNode/SelectionMenu.tsx +++ b/workspace/src/app/views/shared/RuleEditor/view/ruleNode/SelectionMenu.tsx @@ -1,7 +1,6 @@ import React from "react"; -import { EdgeTools } from "@eccenca/gui-elements/src/extensions/react-flow"; import { XYPosition } from "react-flow-renderer/dist/types"; -import { Button, Spacing, Menu, MenuItem, MenuDivider } from "@eccenca/gui-elements"; +import { ContextMenu, MenuItem, MenuDivider } from "@eccenca/gui-elements"; import { useTranslation } from "react-i18next"; interface SelectionMenuProps { @@ -17,20 +16,6 @@ interface SelectionMenuProps { copySelection: () => any; } -/** - * - * - handleDeleteNode(nodeId)} - text={t("WorkflowEditor.node.menu.remove.label")} - intent="danger" - /> - * @returns - */ -/** Rule edge menu. */ export const SelectionMenu = ({ position, onClose, @@ -40,9 +25,22 @@ export const SelectionMenu = ({ }: SelectionMenuProps) => { const [t] = useTranslation(); return ( - // FIXME: CMEM-3742: Use a generic "tools" component or rename EdgeTools - - +
+ } + > -
-
+ + ); }; From 4547a47916058be7e3f0fa32abdf4012b9770c1a Mon Sep 17 00:00:00 2001 From: arausly Date: Wed, 29 Jan 2025 16:14:09 +0100 Subject: [PATCH 15/16] improved shortcut doc, fix group undo --- .../layout/Header/KeyboardShortcutsModal.tsx | 2 ++ .../shared/RuleEditor/model/RuleEditorModel.tsx | 15 +++++++-------- workspace/src/locales/manual/en.json | 8 +++++--- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/workspace/src/app/views/layout/Header/KeyboardShortcutsModal.tsx b/workspace/src/app/views/layout/Header/KeyboardShortcutsModal.tsx index 266becb74a..e698769339 100644 --- a/workspace/src/app/views/layout/Header/KeyboardShortcutsModal.tsx +++ b/workspace/src/app/views/layout/Header/KeyboardShortcutsModal.tsx @@ -57,6 +57,8 @@ const shortcuts: Record { const addOrMergeRuleModelChange = (ruleModelChanges: RuleModelChanges) => { const lastChange = asChangeNodeParameter(ruleUndoStack[ruleUndoStack.length - 1]); const parameterChange = asChangeNodeParameter(ruleModelChanges); + if ( parameterChange && lastChange && @@ -528,6 +529,7 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { ruleUndoStack.push(ruleModelChanges); } else { ruleUndoStack.push(ruleModelChanges); + console.log("Rule undo stack ==>", ruleUndoStack); } }; @@ -1256,12 +1258,9 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { try { const clipboardData = e.clipboardData?.getData("Text"); const pasteInfo = JSON.parse(clipboardData); // Parse JSON - const taskType = - (ruleEditorContext.editedItem as { type: string })?.type === "linking" ? "linking" : "transform"; - - if (pasteInfo[taskType]) { + if (pasteInfo.task) { changeElementsInternal((els) => { - const nodes = pasteInfo[taskType].data.nodes ?? []; + const nodes = pasteInfo.task.data.nodes ?? []; const nodeIdMap = new Map(); const newNodes: RuleEditorNode[] = []; nodes.forEach((node) => { @@ -1288,7 +1287,7 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { } }); const newEdges: Edge[] = []; - pasteInfo[taskType].data.edges.forEach((edge) => { + pasteInfo.task.data.edges.forEach((edge) => { if (nodeIdMap.has(edge.source) && nodeIdMap.has(edge.target)) { const newEdge = utils.createEdge( nodeIdMap.get(edge.source)!!, @@ -1299,7 +1298,7 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { newEdges.push(newEdge); } }); - + startChangeTransaction(); const withNodes = addAndExecuteRuleModelChangeInternal( RuleModelChangesFactory.addNodes(newNodes), els @@ -1358,7 +1357,7 @@ export const RuleEditorModel = ({ children }: RuleEditorModelProps) => { const { projectId, editedItemId, editedItem } = ruleEditorContext; const taskType = (editedItem as { type: string })?.type === "linking" ? "linking" : "transform"; const data = JSON.stringify({ - [taskType]: { + task: { data: { nodes, edges, diff --git a/workspace/src/locales/manual/en.json b/workspace/src/locales/manual/en.json index 342b75fc12..afe7d7ec06 100644 --- a/workspace/src/locales/manual/en.json +++ b/workspace/src/locales/manual/en.json @@ -262,14 +262,16 @@ } }, "rule-editors": { - "label": "Linking editor", + "label": "Linking editor / Value formula editor", "shortcuts": { "duplicate-nodes": "Duplicate selected nodes", "undo": "Undo change", "redo": "Redo change", "delete": "Remove selected elements", "multiselect": "Select multiple nodes", - "multiselectDesc": "Select nodes either by clicking on them or by dragging a selection area" + "multiselectDesc": "Select nodes either by clicking on them or by dragging a selection area", + "copySelectedNodes": "Copy selected nodes to clipboard", + "pasteNodes": "Paste copied nodes" } }, "workflow-editor": { @@ -649,7 +651,7 @@ }, "copy": { "label": "Copy selected nodes to clipboard", - "tooltip": "Copy all selected nodes and inter-connections. Following key combination also triggers this action: CTRL/CMD + c. The copied elements can be inserted as copies via CTRL/CMD + v, this holds true also for editors in different tabs/windows/browsers." + "tooltip": "Copy all selected nodes and inter-connections. Can be also triggered with with following keyboard shortcut: ^Ctrl + c / ⌘Command + c. The copied elements can be inserted as copies with following keyboard shortcut: ^Ctrl + v / ⌘Command + v.The copied elements can also be inserted in editors in different tabs, windows or browsers." } } }, From 43fa4089eed69f253ef3ea8f29f3cdfcaa507342 Mon Sep 17 00:00:00 2001 From: arausly Date: Wed, 29 Jan 2025 16:18:08 +0100 Subject: [PATCH 16/16] force tooltip to open left --- .../src/app/views/shared/RuleEditor/view/RuleEditorToolbar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/workspace/src/app/views/shared/RuleEditor/view/RuleEditorToolbar.tsx b/workspace/src/app/views/shared/RuleEditor/view/RuleEditorToolbar.tsx index cf39a96765..446878733a 100644 --- a/workspace/src/app/views/shared/RuleEditor/view/RuleEditorToolbar.tsx +++ b/workspace/src/app/views/shared/RuleEditor/view/RuleEditorToolbar.tsx @@ -229,6 +229,7 @@ export const RuleEditorToolbar = () => { })} tooltipProps={{ isOpen: true, + placement: "left", }} />