diff --git a/designer/client/src/actions/actionTypes.ts b/designer/client/src/actions/actionTypes.ts index 98eb8dc62f9..eb4d106d699 100644 --- a/designer/client/src/actions/actionTypes.ts +++ b/designer/client/src/actions/actionTypes.ts @@ -10,8 +10,6 @@ export type ActionTypes = | "DELETE_NODES" | "NODES_CONNECTED" | "NODES_DISCONNECTED" - | "NODE_ADDED" - | "NODES_WITH_EDGES_ADDED" | "VALIDATION_RESULT" | "COPY_SELECTION" | "CUT_SELECTION" diff --git a/designer/client/src/actions/nk/node.ts b/designer/client/src/actions/nk/node.ts index d1f80cb4435..73b2aa56276 100644 --- a/designer/client/src/actions/nk/node.ts +++ b/designer/client/src/actions/nk/node.ts @@ -1,12 +1,14 @@ +import { Dictionary } from "lodash"; +import { flushSync } from "react-dom"; +import NodeUtils from "../../components/graph/NodeUtils"; +import { batchGroupBy } from "../../reducers/graph/batchGroupBy"; +import { prepareNewNodesWithLayout } from "../../reducers/graph/utils"; +import { getScenarioGraph } from "../../reducers/selectors/graph"; +import { getProcessDefinitionData } from "../../reducers/selectors/settings"; import { Edge, EdgeType, NodeId, NodeType, ProcessDefinitionData, ValidationResult } from "../../types"; import { ThunkAction } from "../reduxTypes"; -import { layoutChanged, Position } from "./ui/layout"; import { EditNodeAction, EditScenarioLabels, RenameProcessAction } from "./editNode"; -import { getProcessDefinitionData } from "../../reducers/selectors/settings"; -import { batchGroupBy } from "../../reducers/graph/batchGroupBy"; -import NodeUtils from "../../components/graph/NodeUtils"; -import { getScenarioGraph } from "../../reducers/selectors/graph"; -import { flushSync } from "react-dom"; +import { layoutChanged, NodePosition, Position } from "./ui/layout"; export type NodesWithPositions = { node: NodeType; position: Position }[]; @@ -31,7 +33,9 @@ type NodesDisonnectedAction = { type NodesWithEdgesAddedAction = { type: "NODES_WITH_EDGES_ADDED"; - nodesWithPositions: NodesWithPositions; + nodes: NodeType[]; + layout: NodePosition[]; + idMapping: Dictionary; edges: Edge[]; processDefinitionData: ProcessDefinitionData; }; @@ -43,8 +47,8 @@ type ValidationResultAction = { type NodeAddedAction = { type: "NODE_ADDED"; - node: NodeType; - position: Position; + nodes: NodeType[]; + layout: NodePosition[]; }; export function deleteNodes(ids: NodeId[]): ThunkAction { @@ -118,13 +122,20 @@ export function injectNode(from: NodeType, middle: NodeType, to: NodeType, { edg } export function nodeAdded(node: NodeType, position: Position): ThunkAction { - return (dispatch) => { + return (dispatch, getState) => { batchGroupBy.startOrContinue(); // We need to disable automatic React batching https://react.dev/blog/2022/03/29/react-v18#new-feature-automatic-batching // since it breaks redux undo in this case flushSync(() => { - dispatch({ type: "NODE_ADDED", node, position }); + const scenarioGraph = getScenarioGraph(getState()); + const { nodes, layout } = prepareNewNodesWithLayout(scenarioGraph.nodes, [{ node, position }], false); + + dispatch({ + type: "NODE_ADDED", + nodes, + layout, + }); dispatch(layoutChanged()); }); batchGroupBy.end(); @@ -133,11 +144,17 @@ export function nodeAdded(node: NodeType, position: Position): ThunkAction { export function nodesWithEdgesAdded(nodesWithPositions: NodesWithPositions, edges: Edge[]): ThunkAction { return (dispatch, getState) => { - const processDefinitionData = getProcessDefinitionData(getState()); + const state = getState(); + const processDefinitionData = getProcessDefinitionData(state); + const scenarioGraph = getScenarioGraph(state); + const { nodes, layout, idMapping } = prepareNewNodesWithLayout(scenarioGraph.nodes, nodesWithPositions, true); + batchGroupBy.startOrContinue(); dispatch({ type: "NODES_WITH_EDGES_ADDED", - nodesWithPositions, + nodes, + layout, + idMapping, edges, processDefinitionData, }); diff --git a/designer/client/src/common/ClipboardUtils.ts b/designer/client/src/common/ClipboardUtils.ts index e831406d535..b5206185feb 100644 --- a/designer/client/src/common/ClipboardUtils.ts +++ b/designer/client/src/common/ClipboardUtils.ts @@ -17,6 +17,10 @@ export async function readText(event?: Event): Promise { } } +interface WriteText { + (text: string): Promise; +} + // We could have used navigator.clipboard.writeText but it is not defined for // content delivered via HTTP. The workaround is to create a hidden element // and then write text into it. After that copy command is used to replace @@ -24,13 +28,15 @@ export async function readText(event?: Event): Promise { // assigned with given id to be able to differentiate between artificial // copy event and the real one triggered by user. // Based on https://techoverflow.net/2018/03/30/copying-strings-to-the-clipboard-using-pure-javascript/ - -export function writeText(text: string): Promise { +const fallbackWriteText: WriteText = (text) => { return new Promise((resolve) => { const el = document.createElement("textarea"); el.value = text; el.setAttribute("readonly", ""); - el.className = css({ position: "absolute", left: "-9999px" }); + el.className = css({ + position: "absolute", + left: "-9999px", + }); el.oncopy = (e) => { // Skip event triggered by writing selection to the clipboard. e.stopPropagation(); @@ -41,4 +47,11 @@ export function writeText(text: string): Promise { document.execCommand("copy"); document.body.removeChild(el); }); -} +}; + +export const writeText: WriteText = (text) => { + if (navigator.clipboard?.writeText) { + return navigator.clipboard.writeText(text).then(() => text); + } + return fallbackWriteText(text); +}; diff --git a/designer/client/src/components/graph/SelectionContextProvider.tsx b/designer/client/src/components/graph/SelectionContextProvider.tsx index 7643ec2f795..e221a6a9b23 100644 --- a/designer/client/src/components/graph/SelectionContextProvider.tsx +++ b/designer/client/src/components/graph/SelectionContextProvider.tsx @@ -1,3 +1,4 @@ +import { min } from "lodash"; import React, { createContext, PropsWithChildren, @@ -11,6 +12,7 @@ import React, { } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; +import { ActionCreators as UndoActionCreators } from "redux-undo"; import { useDebouncedCallback } from "use-debounce"; import { copySelection, @@ -23,17 +25,15 @@ import { selectAll, } from "../../actions/nk"; import { error, success } from "../../actions/notificationActions"; -import { ActionCreators as UndoActionCreators } from "redux-undo"; import * as ClipboardUtils from "../../common/ClipboardUtils"; import { tryParseOrNull } from "../../common/JsonUtils"; import { isInputEvent } from "../../containers/BindKeyboardShortcuts"; +import { useInterval } from "../../containers/Interval"; import { useDocumentListeners } from "../../containers/useDocumentListeners"; import { canModifySelectedNodes, getSelection, getSelectionState } from "../../reducers/selectors/graph"; import { getCapabilities } from "../../reducers/selectors/other"; import { getProcessDefinitionData } from "../../reducers/selectors/settings"; import NodeUtils from "./NodeUtils"; -import { min } from "lodash"; -import { useInterval } from "../../containers/Interval"; const hasTextSelection = () => !!window.getSelection().toString(); diff --git a/designer/client/src/components/themed/InputWithIcon.tsx b/designer/client/src/components/themed/InputWithIcon.tsx index 6450b08bf58..9b4c8b79b8f 100644 --- a/designer/client/src/components/themed/InputWithIcon.tsx +++ b/designer/client/src/components/themed/InputWithIcon.tsx @@ -72,7 +72,13 @@ export const InputWithIcon = forwardRef(function InputWithIcon
{!!props.value && onClear && ( -
+
{ + onClear(); + focus({ preventScroll: true }); + }} + >
)} diff --git a/designer/client/src/containers/BindKeyboardShortcuts.tsx b/designer/client/src/containers/BindKeyboardShortcuts.tsx index 3fdc794ec06..0552c87800b 100644 --- a/designer/client/src/containers/BindKeyboardShortcuts.tsx +++ b/designer/client/src/containers/BindKeyboardShortcuts.tsx @@ -1,7 +1,7 @@ import { useCallback, useMemo } from "react"; import { useSelectionActions } from "../components/graph/SelectionContextProvider"; +import { EventTrackingSelector, EventTrackingType, TrackEventParams, useEventTracking } from "./event-tracking"; import { useDocumentListeners } from "./useDocumentListeners"; -import { EventTrackingSelector, TrackEventParams, EventTrackingType, useEventTracking } from "./event-tracking"; export const isInputTarget = (target: EventTarget): boolean => ["INPUT", "SELECT", "TEXTAREA"].includes(target?.["tagName"]); export const isInputEvent = (event: Event): boolean => isInputTarget(event?.target); @@ -46,12 +46,17 @@ export function BindKeyboardShortcuts({ disabled }: { disabled?: boolean }): JSX if (isInputEvent(event) || !keyHandler) return; return keyHandler(event); }, - copy: (event) => - userActions.copy ? eventWithStatistics({ selector: EventTrackingSelector.CopyNode }, userActions.copy(event)) : null, - paste: (event) => - userActions.paste ? eventWithStatistics({ selector: EventTrackingSelector.PasteNode }, userActions.paste(event)) : null, - cut: (event) => - userActions.cut ? eventWithStatistics({ selector: EventTrackingSelector.CutNode }, userActions.cut(event)) : null, + copy: (event) => { + if (isInputEvent(event)) return; + userActions.copy ? eventWithStatistics({ selector: EventTrackingSelector.CopyNode }, userActions.copy(event)) : null; + }, + paste: (event) => { + userActions.paste ? eventWithStatistics({ selector: EventTrackingSelector.PasteNode }, userActions.paste(event)) : null; + }, + cut: (event) => { + if (isInputEvent(event)) return; + userActions.cut ? eventWithStatistics({ selector: EventTrackingSelector.CutNode }, userActions.cut(event)) : null; + }, }), [eventWithStatistics, keyHandlers, userActions], ); diff --git a/designer/client/src/reducers/graph/__snapshots__/utils.test.ts.snap b/designer/client/src/reducers/graph/__snapshots__/utils.test.ts.snap new file mode 100644 index 00000000000..ccc42518bd1 --- /dev/null +++ b/designer/client/src/reducers/graph/__snapshots__/utils.test.ts.snap @@ -0,0 +1,110 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GraphUtils prepareNewNodesWithLayout should update union output expression parameter with an updated node name when new unique node ids created 1`] = ` +{ + "idMapping": { + "union": "union (copy 1)", + "variable 1": "variable 1 (copy 1)", + "variable 2": "variable 2 (copy 1)", + }, + "layout": [ + { + "id": "variable 1 (copy 1)", + "position": { + "x": 350, + "y": 859, + }, + }, + { + "id": "variable 2 (copy 1)", + "position": { + "x": 710, + "y": 859, + }, + }, + { + "id": "union (copy 1)", + "position": { + "x": 530, + "y": 1039, + }, + }, + ], + "nodes": [ + { + "additionalFields": { + "description": null, + "layoutData": { + "x": 0, + "y": 720, + }, + }, + "branchParameters": undefined, + "id": "variable 1 (copy 1)", + "type": "Variable", + "value": { + "expression": "'value'", + "language": "spel", + }, + "varName": "varName1", + }, + { + "additionalFields": { + "description": null, + "layoutData": { + "x": 360, + "y": 720, + }, + }, + "branchParameters": undefined, + "id": "variable 2 (copy 1)", + "type": "Variable", + "value": { + "expression": "'value'", + "language": "spel", + }, + "varName": "varName2", + }, + { + "additionalFields": { + "description": null, + "layoutData": { + "x": 180, + "y": 900, + }, + }, + "branchParameters": [ + { + "branchId": "variable 1 (copy 1)", + "parameters": [ + { + "expression": { + "expression": "1", + "language": "spel", + }, + "name": "Output expression", + }, + ], + }, + { + "branchId": "variable 2 (copy 1)", + "parameters": [ + { + "expression": { + "expression": "2", + "language": "spel", + }, + "name": "Output expression", + }, + ], + }, + ], + "id": "union (copy 1)", + "nodeType": "union", + "outputVar": "outputVar", + "parameters": [], + "type": "Join", + }, + ], +} +`; diff --git a/designer/client/src/reducers/graph/reducer.ts b/designer/client/src/reducers/graph/reducer.ts index 0e9e1de8aa5..a673de82d18 100644 --- a/designer/client/src/reducers/graph/reducer.ts +++ b/designer/client/src/reducers/graph/reducer.ts @@ -1,27 +1,27 @@ /* eslint-disable i18next/no-literal-string */ -import { concat, defaultsDeep, isEqual, omit as _omit, pick as _pick, sortBy, uniq, xor, zipObject } from "lodash"; +import { concat, defaultsDeep, isEqual, omit as _omit, pick as _pick, sortBy } from "lodash"; import undoable, { ActionTypes as UndoActionTypes, combineFilters, excludeAction, StateWithHistory } from "redux-undo"; import { Action, Reducer } from "../../actions/reduxTypes"; +import ProcessUtils from "../../common/ProcessUtils"; +import NodeUtils from "../../components/graph/NodeUtils"; import * as GraphUtils from "../../components/graph/utils/graphUtils"; +import { ValidationResult } from "../../types"; import * as LayoutUtils from "../layoutUtils"; import { nodes } from "../layoutUtils"; import { mergeReducers } from "../mergeReducers"; +import { batchGroupBy } from "./batchGroupBy"; +import { correctFetchedDetails } from "./correctFetchedDetails"; +import { NestedKeyOf } from "./nestedKeyOf"; +import { selectionState } from "./selectionState"; import { GraphState } from "./types"; import { addNodesWithLayout, adjustBranchParametersAfterDisconnect, createEdge, enrichNodeWithProcessDependentData, - prepareNewNodesWithLayout, updateAfterNodeDelete, updateLayoutAfterNodeIdChange, } from "./utils"; -import { ValidationResult } from "../../types"; -import NodeUtils from "../../components/graph/NodeUtils"; -import { batchGroupBy } from "./batchGroupBy"; -import { NestedKeyOf } from "./nestedKeyOf"; -import ProcessUtils from "../../common/ProcessUtils"; -import { correctFetchedDetails } from "./correctFetchedDetails"; //TODO: We should change namespace from graphReducer to currentlyDisplayedProcess @@ -223,49 +223,33 @@ const graphReducer: Reducer = (state = emptyGraphState, action) => { }; } case "NODE_ADDED": { - const nodeWithPosition = { - node: action.node, - position: action.position, - }; - const { uniqueIds, nodes, layout } = prepareNewNodesWithLayout(state, [nodeWithPosition], false); - return { - ...addNodesWithLayout(state, { nodes, layout }), - selectionState: uniqueIds, - }; + return addNodesWithLayout(state, { + nodes: action.nodes, + layout: action.layout, + }); } case "NODES_WITH_EDGES_ADDED": { - const { nodes, layout, uniqueIds } = prepareNewNodesWithLayout(state, action.nodesWithPositions, true); + const { nodes, layout, idMapping, processDefinitionData, edges } = action; - const idToUniqueId = zipObject( - action.nodesWithPositions.map((n) => n.node.id), - uniqueIds, - ); - const edgesWithValidIds = action.edges.map((edge) => ({ + const edgesWithValidIds = edges.map((edge) => ({ ...edge, - from: idToUniqueId[edge.from], - to: idToUniqueId[edge.to], + from: idMapping[edge.from], + to: idMapping[edge.to], })); - const updatedEdges = edgesWithValidIds.reduce((edges, edge) => { + const adjustedEdges = edgesWithValidIds.reduce((edges, edge) => { const fromNode = nodes.find((n) => n.id === edge.from); const toNode = nodes.find((n) => n.id === edge.to); const currentNodeEdges = NodeUtils.getOutputEdges(fromNode.id, edges); - const newEdge = createEdge(fromNode, toNode, edge.edgeType, currentNodeEdges, action.processDefinitionData); + const newEdge = createEdge(fromNode, toNode, edge.edgeType, currentNodeEdges, processDefinitionData); return edges.concat(newEdge); }, state.scenario.scenarioGraph.edges); - const stateWithNodesAdded = addNodesWithLayout(state, { nodes, layout }); - return { - ...stateWithNodesAdded, - scenario: { - ...stateWithNodesAdded.scenario, - scenarioGraph: { - ...stateWithNodesAdded.scenario.scenarioGraph, - edges: updatedEdges, - }, - }, - selectionState: uniqueIds, - }; + return addNodesWithLayout(state, { + nodes, + layout, + edges: adjustedEdges, + }); } case "VALIDATION_RESULT": { return { @@ -305,24 +289,6 @@ const graphReducer: Reducer = (state = emptyGraphState, action) => { processCountsRefresh: null, }; } - case "EXPAND_SELECTION": { - return { - ...state, - selectionState: uniq([...state.selectionState, ...action.nodeIds]), - }; - } - case "TOGGLE_SELECTION": { - return { - ...state, - selectionState: xor(state.selectionState, action.nodeIds), - }; - } - case "RESET_SELECTION": { - return { - ...state, - selectionState: action.nodeIds ? action.nodeIds : [], - }; - } default: return state; } @@ -334,6 +300,7 @@ const reducer: Reducer = mergeReducers(graphReducer, { nodes, }, }, + selectionState, }); const pick = >(object: T, props: NestedKeyOf[]) => _pick(object, props); diff --git a/designer/client/src/reducers/graph/selectionState.ts b/designer/client/src/reducers/graph/selectionState.ts new file mode 100644 index 00000000000..842c439f18d --- /dev/null +++ b/designer/client/src/reducers/graph/selectionState.ts @@ -0,0 +1,19 @@ +import { uniq, xor } from "lodash"; +import { Reducer } from "../../actions/reduxTypes"; + +export const selectionState: Reducer = (state = [], action) => { + switch (action.type) { + case "NODES_WITH_EDGES_ADDED": + case "NODE_ADDED": + return action.nodes.map((n) => n.id); + case "DELETE_NODES": + return xor(state, action.ids); + case "EXPAND_SELECTION": + return uniq([...state, ...action.nodeIds]); + case "TOGGLE_SELECTION": + return xor(state, action.nodeIds); + case "RESET_SELECTION": + return action.nodeIds ? action.nodeIds : []; + } + return state; +}; diff --git a/designer/client/src/reducers/graph/utils.test.ts b/designer/client/src/reducers/graph/utils.test.ts index fb1e6bdb692..6888a23c8f4 100644 --- a/designer/client/src/reducers/graph/utils.test.ts +++ b/designer/client/src/reducers/graph/utils.test.ts @@ -1,222 +1,9 @@ import { prepareNewNodesWithLayout } from "./utils"; import { nodesWithPositions, state } from "./utils.fixtures"; -describe("GraphUtils prepareNewNodesWithLayout", () => { - it("should update union output expression parameter with an updated node name when new unique node ids created", () => { - const expected = { - layout: [ - { - id: "choice", - position: { - x: 180, - y: 540, - }, - }, - { - id: "variable 1", - position: { - x: 0, - y: 720, - }, - }, - { - id: "variable 2", - position: { - x: 360, - y: 720, - }, - }, - { - id: "union", - position: { - x: 180, - y: 900, - }, - }, - { - id: "variable 1 (copy 1)", - position: { - x: 350, - y: 859, - }, - }, - { - id: "variable 2 (copy 1)", - position: { - x: 710, - y: 859, - }, - }, - { - id: "union (copy 1)", - position: { - x: 530, - y: 1039, - }, - }, - ], - nodes: [ - { - additionalFields: { - description: null, - layoutData: { - x: 180, - y: 540, - }, - }, - exprVal: null, - expression: null, - id: "choice", - type: "Switch", - }, - { - additionalFields: { - description: null, - layoutData: { - x: 0, - y: 720, - }, - }, - id: "variable 1", - type: "Variable", - value: { - expression: "'value'", - language: "spel", - }, - varName: "varName1", - }, - { - additionalFields: { - description: null, - layoutData: { - x: 360, - y: 720, - }, - }, - id: "variable 2", - type: "Variable", - value: { - expression: "'value'", - language: "spel", - }, - varName: "varName2", - }, - { - additionalFields: { - description: null, - layoutData: { - x: 180, - y: 900, - }, - }, - branchParameters: [ - { - branchId: "variable 1", - parameters: [ - { - expression: { - expression: "1", - language: "spel", - }, - name: "Output expression", - }, - ], - }, - { - branchId: "variable 2", - parameters: [ - { - expression: { - expression: "2", - language: "spel", - }, - name: "Output expression", - }, - ], - }, - ], - id: "union", - nodeType: "union", - outputVar: "outputVar", - parameters: [], - type: "Join", - }, - { - additionalFields: { - description: null, - layoutData: { - x: 0, - y: 720, - }, - }, - id: "variable 1 (copy 1)", - type: "Variable", - value: { - expression: "'value'", - language: "spel", - }, - varName: "varName1", - }, - { - additionalFields: { - description: null, - layoutData: { - x: 360, - y: 720, - }, - }, - id: "variable 2 (copy 1)", - type: "Variable", - value: { - expression: "'value'", - language: "spel", - }, - varName: "varName2", - }, - { - additionalFields: { - description: null, - layoutData: { - x: 180, - y: 900, - }, - }, - branchParameters: [ - { - branchId: "variable 1 (copy 1)", - parameters: [ - { - expression: { - expression: "1", - language: "spel", - }, - name: "Output expression", - }, - ], - }, - { - branchId: "variable 2 (copy 1)", - parameters: [ - { - expression: { - expression: "2", - language: "spel", - }, - name: "Output expression", - }, - ], - }, - ], - id: "union (copy 1)", - nodeType: "union", - outputVar: "outputVar", - parameters: [], - type: "Join", - }, - ], - uniqueIds: ["variable 1 (copy 1)", "variable 2 (copy 1)", "union (copy 1)"], - }; - - expect(prepareNewNodesWithLayout(state, nodesWithPositions, true)).toEqual(expected); +describe("GraphUtils", () => { + it("prepareNewNodesWithLayout should update union output expression parameter with an updated node name when new unique node ids created", () => { + const { scenarioGraph } = state.scenario; + expect(prepareNewNodesWithLayout(scenarioGraph.nodes, nodesWithPositions, true)).toMatchSnapshot(); }); }); diff --git a/designer/client/src/reducers/graph/utils.ts b/designer/client/src/reducers/graph/utils.ts index ba5f7cb9ea8..4ddbff02953 100644 --- a/designer/client/src/reducers/graph/utils.ts +++ b/designer/client/src/reducers/graph/utils.ts @@ -1,4 +1,4 @@ -import { cloneDeep, map, reject, zipWith } from "lodash"; +import { cloneDeep, Dictionary, map, reject, zipObject, zipWith } from "lodash"; import { Layout, NodePosition, NodesWithPositions } from "../../actions/nk"; import ProcessUtils from "../../common/ProcessUtils"; import { ExpressionLang } from "../../components/graph/node-modal/editors/expression/types"; @@ -41,56 +41,63 @@ function getUniqueIds(initialIds: string[], alreadyUsedIds: string[], isCopy: bo }, []); } +function adjustBranchParameters(branchParameters: BranchParams[], uniqueIds: string[]) { + return branchParameters?.map(({ branchId, ...branchParameter }: BranchParams) => ({ + ...branchParameter, + branchId: uniqueIds.find((uniqueId) => uniqueId.includes(branchId)), + })); +} + export function prepareNewNodesWithLayout( - state: GraphState, - nodesWithPositions: NodesWithPositions, + currentNodes: NodeType[] = [], + newNodesWithPositions: NodesWithPositions, isCopy: boolean, -): { layout: NodePosition[]; nodes: NodeType[]; uniqueIds?: NodeId[] } { - const { - layout, - scenario: { - scenarioGraph: { nodes = [] }, - }, - } = state; - - const alreadyUsedIds = nodes.map((node) => node.id); - const initialIds = nodesWithPositions.map((nodeWithPosition) => nodeWithPosition.node.id); +): { + layout: NodePosition[]; + nodes: NodeType[]; + idMapping: Dictionary; +} { + const newNodes = newNodesWithPositions.map(({ node }) => node); + const newPositions = newNodesWithPositions.map(({ position }) => position); + const alreadyUsedIds = currentNodes.map((node) => node.id); + const initialIds = newNodes.map(({ id }) => id); const uniqueIds = getUniqueIds(initialIds, alreadyUsedIds, isCopy); - const updatedNodes = zipWith(nodesWithPositions, uniqueIds, ({ node }, id) => { - const nodeCopy = cloneDeep(node); - const adjustBranchParametersToTheCopiedElements = (branchParameter: BranchParams) => { - branchParameter.branchId = uniqueIds.find((uniqueId) => uniqueId.includes(branchParameter.branchId)); - return branchParameter; - }; - - if (nodeCopy.branchParameters) { - nodeCopy.branchParameters = nodeCopy.branchParameters.map(adjustBranchParametersToTheCopiedElements); - } - - nodeCopy.id = id; - return nodeCopy; - }); - const updatedLayout = zipWith(nodesWithPositions, uniqueIds, ({ position }, id) => ({ id, position })); - return { - nodes: [...nodes, ...updatedNodes], - layout: [...layout, ...updatedLayout], - uniqueIds, + nodes: zipWith(newNodes, uniqueIds, (node, id) => ({ + ...node, + id, + branchParameters: adjustBranchParameters(node.branchParameters, uniqueIds), + })), + layout: zipWith(newPositions, uniqueIds, (position, id) => ({ + id, + position, + })), + idMapping: zipObject(initialIds, uniqueIds), }; } -export function addNodesWithLayout(state: GraphState, { nodes, layout }: ReturnType): GraphState { +export function addNodesWithLayout( + state: GraphState, + changes: { + nodes: NodeType[]; + layout: NodePosition[]; + edges?: Edge[]; + }, +): GraphState { + const currentNodes = state.scenario.scenarioGraph.nodes || []; + const currentLayout = state.layout; return { ...state, scenario: { ...state.scenario, scenarioGraph: { ...state.scenario.scenarioGraph, - nodes, + nodes: [...currentNodes, ...changes.nodes], + edges: changes.edges, }, }, - layout, + layout: [...currentLayout, ...changes.layout], }; } diff --git a/designer/client/src/reducers/settings.ts b/designer/client/src/reducers/settings.ts index 63363376853..0e57f5df7cb 100644 --- a/designer/client/src/reducers/settings.ts +++ b/designer/client/src/reducers/settings.ts @@ -54,7 +54,9 @@ const initialState: SettingsState = { loggedUser: {}, featuresSettings: {}, authenticationSettings: {}, - processDefinitionData: {}, + processDefinitionData: { + edgesForNodes: [], + }, processToolbarsConfiguration: null, }; diff --git a/designer/client/test/__snapshots__/reducer-test.js.snap b/designer/client/test/__snapshots__/reducer-test.js.snap new file mode 100644 index 00000000000..12e43d29455 --- /dev/null +++ b/designer/client/test/__snapshots__/reducer-test.js.snap @@ -0,0 +1,163 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Nodes added should add multiple nodes with unique id 1`] = ` +{ + "additionalFields": { + "layoutData": { + "x": 10, + "y": 20, + }, + }, + "branchParameters": undefined, + "id": "kafka-transaction (copy 1)", + "output": "output", + "service": { + "id": "paramService", + "parameters": [ + { + "expression": { + "expression": "'3434'", + "language": "spel", + }, + "name": "param", + }, + ], + }, + "type": "Enricher", +} +`; + +exports[`Nodes added should add multiple nodes with unique id 2`] = ` +{ + "additionalFields": { + "layoutData": { + "x": 10, + "y": 20, + }, + }, + "branchParameters": undefined, + "id": "kafka-transaction (copy 2)", + "output": "output", + "service": { + "id": "paramService", + "parameters": [ + { + "expression": { + "expression": "'3434'", + "language": "spel", + }, + "name": "param", + }, + ], + }, + "type": "Enricher", +} +`; + +exports[`Nodes added should add nodes with edges 1`] = ` +{ + "additionalFields": { + "layoutData": { + "x": 10, + "y": 20, + }, + }, + "branchParameters": undefined, + "id": "newNode", + "output": "output", + "service": { + "id": "paramService", + "parameters": [ + { + "expression": { + "expression": "'3434'", + "language": "spel", + }, + "name": "param", + }, + ], + }, + "type": "Enricher", +} +`; + +exports[`Nodes added should add nodes with edges 2`] = ` +{ + "additionalFields": { + "layoutData": { + "x": 10, + "y": 20, + }, + }, + "branchParameters": undefined, + "id": "kafka-transaction (copy 1)", + "output": "output", + "service": { + "id": "paramService", + "parameters": [ + { + "expression": { + "expression": "'3434'", + "language": "spel", + }, + "name": "param", + }, + ], + }, + "type": "Enricher", +} +`; + +exports[`Nodes added should add single node 1`] = ` +{ + "additionalFields": { + "layoutData": { + "x": 10, + "y": 20, + }, + }, + "branchParameters": undefined, + "id": "Enricher ID", + "output": "output", + "service": { + "id": "paramService", + "parameters": [ + { + "expression": { + "expression": "'3434'", + "language": "spel", + }, + "name": "param", + }, + ], + }, + "type": "Enricher", +} +`; + +exports[`Nodes added should add single node with unique id 1`] = ` +{ + "additionalFields": { + "layoutData": { + "x": 10, + "y": 20, + }, + }, + "branchParameters": undefined, + "id": "kafka-transaction 1", + "output": "output", + "service": { + "id": "paramService", + "parameters": [ + { + "expression": { + "expression": "'3434'", + "language": "spel", + }, + "name": "param", + }, + ], + }, + "type": "Enricher", +} +`; diff --git a/designer/client/test/reducer-test.js b/designer/client/test/reducer-test.js index cc842d343d5..2e38c98361d 100644 --- a/designer/client/test/reducer-test.js +++ b/designer/client/test/reducer-test.js @@ -1,5 +1,6 @@ -import reducer from "../src/reducers/index"; +import { nodeAdded, nodesWithEdgesAdded } from "../src/actions/nk"; import NodeUtils from "../src/components/graph/NodeUtils"; +import reducer from "../src/reducers/index"; const baseProcessState = { name: "DEFGH", @@ -108,7 +109,22 @@ const baseStateWithProcess = reducer(baseState, { scenario: baseProcessState, }); -const reduceAll = (actions) => actions.reduce((state, action) => reducer(state, action), baseStateWithProcess); +const reduceAll = (actions) => { + let currentState = baseStateWithProcess; + const getState = () => currentState; + + const dispatch = (action) => { + if (typeof action === "function") { + action(dispatch, getState); + } else { + currentState = reducer(currentState, action); + } + }; + + actions.forEach((action) => dispatch(action)); + + return currentState; +}; describe("Reducer suite", () => { it("Display process", () => { @@ -138,39 +154,23 @@ const testPosition = { x: 10, y: 20 }; describe("Nodes added", () => { it("should add single node", () => { - const result = reduceAll([ - { - type: "NODE_ADDED", - node: testNode, - position: testPosition, - }, - ]); + const result = reduceAll([nodeAdded(testNode, testPosition)]); - expect(NodeUtils.getNodeById(testNode.id, result.graphReducer.scenario.scenarioGraph)).toEqual(testNode); + expect(NodeUtils.getNodeById(testNode.id, result.graphReducer.scenario.scenarioGraph)).toMatchSnapshot(); expect(result.graphReducer.layout.find((n) => n.id === testNode.id).position).toEqual(testPosition); }); it("should add single node with unique id", () => { - const result = reduceAll([ - { - type: "NODE_ADDED", - node: { ...testNode, id: "kafka-transaction" }, - position: testPosition, - }, - ]); + const result = reduceAll([nodeAdded({ ...testNode, id: "kafka-transaction" }, testPosition)]); - expect(NodeUtils.getNodeById("kafka-transaction 1", result.graphReducer.scenario.scenarioGraph)).toEqual({ - ...testNode, - id: "kafka-transaction 1", - }); - expect(result.graphReducer.layout.find((n) => n.id).position).toEqual(testPosition); + expect(NodeUtils.getNodeById("kafka-transaction 1", result.graphReducer.scenario.scenarioGraph)).toMatchSnapshot(); + expect(result.graphReducer.layout.find((n) => n.id === "kafka-transaction 1").position).toEqual(testPosition); }); it("should add multiple nodes with unique id", () => { const result = reduceAll([ - { - type: "NODES_WITH_EDGES_ADDED", - nodesWithPositions: [ + nodesWithEdgesAdded( + [ { node: { ...testNode, id: "kafka-transaction" }, position: testPosition, @@ -180,25 +180,18 @@ describe("Nodes added", () => { position: testPosition, }, ], - edges: [], - }, + [], + ), ]); - expect(NodeUtils.getNodeById("kafka-transaction (copy 1)", result.graphReducer.scenario.scenarioGraph)).toEqual({ - ...testNode, - id: "kafka-transaction (copy 1)", - }); - expect(NodeUtils.getNodeById("kafka-transaction (copy 2)", result.graphReducer.scenario.scenarioGraph)).toEqual({ - ...testNode, - id: "kafka-transaction (copy 2)", - }); + expect(NodeUtils.getNodeById("kafka-transaction (copy 1)", result.graphReducer.scenario.scenarioGraph)).toMatchSnapshot(); + expect(NodeUtils.getNodeById("kafka-transaction (copy 2)", result.graphReducer.scenario.scenarioGraph)).toMatchSnapshot(); }); it("should add nodes with edges", () => { const result = reduceAll([ - { - type: "NODES_WITH_EDGES_ADDED", - nodesWithPositions: [ + nodesWithEdgesAdded( + [ { node: { ...testNode, id: "newNode" }, position: testPosition, @@ -208,18 +201,12 @@ describe("Nodes added", () => { position: testPosition, }, ], - edges: [{ from: "newNode", to: "kafka-transaction" }], - processDefinitionData: { - edgesForNodes: [], - }, - }, + [{ from: "newNode", to: "kafka-transaction" }], + ), ]); - expect(NodeUtils.getNodeById("newNode", result.graphReducer.scenario.scenarioGraph)).toEqual({ ...testNode, id: "newNode" }); - expect(NodeUtils.getNodeById("kafka-transaction (copy 1)", result.graphReducer.scenario.scenarioGraph)).toEqual({ - ...testNode, - id: "kafka-transaction (copy 1)", - }); + expect(NodeUtils.getNodeById("newNode", result.graphReducer.scenario.scenarioGraph)).toMatchSnapshot(); + expect(NodeUtils.getNodeById("kafka-transaction (copy 1)", result.graphReducer.scenario.scenarioGraph)).toMatchSnapshot(); expect(NodeUtils.getEdgeById("newNode-kafka-transaction (copy 1)", result.graphReducer.scenario.scenarioGraph)).toEqual({ from: "newNode", to: "kafka-transaction (copy 1)", diff --git a/docs/Changelog.md b/docs/Changelog.md index f88ca4e2a3c..ee7b1ba8549 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -109,6 +109,7 @@ * [#7192](https://github.com/TouK/nussknacker/pull/7192) Fix "Failed to get node validation" when opening node details referencing non-existing component * [#7190](https://github.com/TouK/nussknacker/pull/7190) Fix "Failed to get node validation" when opening fragment node details for referencing non-existing fragment * [#7215](https://github.com/TouK/nussknacker/pull/7215) Change typing text to spinner during validation and provide delayed adding on enter until validation finishes in a scenario labels and fragment input +* [#7207](https://github.com/TouK/nussknacker/pull/7207) Fixed minor clipboard, keyboard and focus related bugs ## 1.17