Skip to content

Commit f080e3e

Browse files
committed
fixed search focus and keyboard/clipboard events
separate selectionState reducer
1 parent 8a6c1cf commit f080e3e

File tree

14 files changed

+469
-388
lines changed

14 files changed

+469
-388
lines changed

designer/client/src/actions/actionTypes.ts

-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ export type ActionTypes =
1010
| "DELETE_NODES"
1111
| "NODES_CONNECTED"
1212
| "NODES_DISCONNECTED"
13-
| "NODE_ADDED"
14-
| "NODES_WITH_EDGES_ADDED"
1513
| "VALIDATION_RESULT"
1614
| "COPY_SELECTION"
1715
| "CUT_SELECTION"

designer/client/src/actions/nk/node.ts

+30-13
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
import { Dictionary } from "lodash";
2+
import { flushSync } from "react-dom";
3+
import NodeUtils from "../../components/graph/NodeUtils";
4+
import { batchGroupBy } from "../../reducers/graph/batchGroupBy";
5+
import { prepareNewNodesWithLayout } from "../../reducers/graph/utils";
6+
import { getScenarioGraph } from "../../reducers/selectors/graph";
7+
import { getProcessDefinitionData } from "../../reducers/selectors/settings";
18
import { Edge, EdgeType, NodeId, NodeType, ProcessDefinitionData, ValidationResult } from "../../types";
29
import { ThunkAction } from "../reduxTypes";
3-
import { layoutChanged, Position } from "./ui/layout";
410
import { EditNodeAction, EditScenarioLabels, RenameProcessAction } from "./editNode";
5-
import { getProcessDefinitionData } from "../../reducers/selectors/settings";
6-
import { batchGroupBy } from "../../reducers/graph/batchGroupBy";
7-
import NodeUtils from "../../components/graph/NodeUtils";
8-
import { getScenarioGraph } from "../../reducers/selectors/graph";
9-
import { flushSync } from "react-dom";
11+
import { layoutChanged, NodePosition, Position } from "./ui/layout";
1012

1113
export type NodesWithPositions = { node: NodeType; position: Position }[];
1214

@@ -31,7 +33,9 @@ type NodesDisonnectedAction = {
3133

3234
type NodesWithEdgesAddedAction = {
3335
type: "NODES_WITH_EDGES_ADDED";
34-
nodesWithPositions: NodesWithPositions;
36+
nodes: NodeType[];
37+
layout: NodePosition[];
38+
idMapping: Dictionary<string>;
3539
edges: Edge[];
3640
processDefinitionData: ProcessDefinitionData;
3741
};
@@ -43,8 +47,8 @@ type ValidationResultAction = {
4347

4448
type NodeAddedAction = {
4549
type: "NODE_ADDED";
46-
node: NodeType;
47-
position: Position;
50+
nodes: NodeType[];
51+
layout: NodePosition[];
4852
};
4953

5054
export function deleteNodes(ids: NodeId[]): ThunkAction {
@@ -118,13 +122,20 @@ export function injectNode(from: NodeType, middle: NodeType, to: NodeType, { edg
118122
}
119123

120124
export function nodeAdded(node: NodeType, position: Position): ThunkAction {
121-
return (dispatch) => {
125+
return (dispatch, getState) => {
122126
batchGroupBy.startOrContinue();
123127

124128
// We need to disable automatic React batching https://react.dev/blog/2022/03/29/react-v18#new-feature-automatic-batching
125129
// since it breaks redux undo in this case
126130
flushSync(() => {
127-
dispatch({ type: "NODE_ADDED", node, position });
131+
const scenarioGraph = getScenarioGraph(getState());
132+
const { nodes, layout } = prepareNewNodesWithLayout(scenarioGraph.nodes, [{ node, position }], false);
133+
134+
dispatch({
135+
type: "NODE_ADDED",
136+
nodes,
137+
layout,
138+
});
128139
dispatch(layoutChanged());
129140
});
130141
batchGroupBy.end();
@@ -133,11 +144,17 @@ export function nodeAdded(node: NodeType, position: Position): ThunkAction {
133144

134145
export function nodesWithEdgesAdded(nodesWithPositions: NodesWithPositions, edges: Edge[]): ThunkAction {
135146
return (dispatch, getState) => {
136-
const processDefinitionData = getProcessDefinitionData(getState());
147+
const state = getState();
148+
const processDefinitionData = getProcessDefinitionData(state);
149+
const scenarioGraph = getScenarioGraph(state);
150+
const { nodes, layout, idMapping } = prepareNewNodesWithLayout(scenarioGraph.nodes, nodesWithPositions, true);
151+
137152
batchGroupBy.startOrContinue();
138153
dispatch({
139154
type: "NODES_WITH_EDGES_ADDED",
140-
nodesWithPositions,
155+
nodes,
156+
layout,
157+
idMapping,
141158
edges,
142159
processDefinitionData,
143160
});

designer/client/src/common/ClipboardUtils.ts

+17-4
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,26 @@ export async function readText(event?: Event): Promise<string> {
1717
}
1818
}
1919

20+
interface WriteText {
21+
(text: string): Promise<string>;
22+
}
23+
2024
// We could have used navigator.clipboard.writeText but it is not defined for
2125
// content delivered via HTTP. The workaround is to create a hidden element
2226
// and then write text into it. After that copy command is used to replace
2327
// clipboard's contents with given text. What is more the hidden element is
2428
// assigned with given id to be able to differentiate between artificial
2529
// copy event and the real one triggered by user.
2630
// Based on https://techoverflow.net/2018/03/30/copying-strings-to-the-clipboard-using-pure-javascript/
27-
28-
export function writeText(text: string): Promise<string> {
31+
const fallbackWriteText: WriteText = (text) => {
2932
return new Promise((resolve) => {
3033
const el = document.createElement("textarea");
3134
el.value = text;
3235
el.setAttribute("readonly", "");
33-
el.className = css({ position: "absolute", left: "-9999px" });
36+
el.className = css({
37+
position: "absolute",
38+
left: "-9999px",
39+
});
3440
el.oncopy = (e) => {
3541
// Skip event triggered by writing selection to the clipboard.
3642
e.stopPropagation();
@@ -41,4 +47,11 @@ export function writeText(text: string): Promise<string> {
4147
document.execCommand("copy");
4248
document.body.removeChild(el);
4349
});
44-
}
50+
};
51+
52+
export const writeText: WriteText = (text) => {
53+
if (navigator.clipboard?.writeText) {
54+
return navigator.clipboard.writeText(text).then(() => text);
55+
}
56+
return fallbackWriteText(text);
57+
};

designer/client/src/components/graph/SelectionContextProvider.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { min } from "lodash";
12
import React, {
23
createContext,
34
PropsWithChildren,
@@ -11,6 +12,7 @@ import React, {
1112
} from "react";
1213
import { useTranslation } from "react-i18next";
1314
import { useDispatch, useSelector } from "react-redux";
15+
import { ActionCreators as UndoActionCreators } from "redux-undo";
1416
import { useDebouncedCallback } from "use-debounce";
1517
import {
1618
copySelection,
@@ -23,17 +25,15 @@ import {
2325
selectAll,
2426
} from "../../actions/nk";
2527
import { error, success } from "../../actions/notificationActions";
26-
import { ActionCreators as UndoActionCreators } from "redux-undo";
2728
import * as ClipboardUtils from "../../common/ClipboardUtils";
2829
import { tryParseOrNull } from "../../common/JsonUtils";
2930
import { isInputEvent } from "../../containers/BindKeyboardShortcuts";
31+
import { useInterval } from "../../containers/Interval";
3032
import { useDocumentListeners } from "../../containers/useDocumentListeners";
3133
import { canModifySelectedNodes, getSelection, getSelectionState } from "../../reducers/selectors/graph";
3234
import { getCapabilities } from "../../reducers/selectors/other";
3335
import { getProcessDefinitionData } from "../../reducers/selectors/settings";
3436
import NodeUtils from "./NodeUtils";
35-
import { min } from "lodash";
36-
import { useInterval } from "../../containers/Interval";
3737

3838
const hasTextSelection = () => !!window.getSelection().toString();
3939

designer/client/src/components/themed/InputWithIcon.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,13 @@ export const InputWithIcon = forwardRef<Focusable, Props>(function InputWithIcon
7272
<ThemedInput ref={ref} {...props} />
7373
<div className={addonWrapperStyles}>
7474
{!!props.value && onClear && (
75-
<div className={addonStyles} onClick={onClear}>
75+
<div
76+
className={addonStyles}
77+
onClick={() => {
78+
onClear();
79+
focus({ preventScroll: true });
80+
}}
81+
>
7682
<ClearIcon />
7783
</div>
7884
)}

designer/client/src/containers/BindKeyboardShortcuts.tsx

+12-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useCallback, useMemo } from "react";
22
import { useSelectionActions } from "../components/graph/SelectionContextProvider";
3+
import { EventTrackingSelector, EventTrackingType, TrackEventParams, useEventTracking } from "./event-tracking";
34
import { useDocumentListeners } from "./useDocumentListeners";
4-
import { EventTrackingSelector, TrackEventParams, EventTrackingType, useEventTracking } from "./event-tracking";
55

66
export const isInputTarget = (target: EventTarget): boolean => ["INPUT", "SELECT", "TEXTAREA"].includes(target?.["tagName"]);
77
export const isInputEvent = (event: Event): boolean => isInputTarget(event?.target);
@@ -46,12 +46,17 @@ export function BindKeyboardShortcuts({ disabled }: { disabled?: boolean }): JSX
4646
if (isInputEvent(event) || !keyHandler) return;
4747
return keyHandler(event);
4848
},
49-
copy: (event) =>
50-
userActions.copy ? eventWithStatistics({ selector: EventTrackingSelector.CopyNode }, userActions.copy(event)) : null,
51-
paste: (event) =>
52-
userActions.paste ? eventWithStatistics({ selector: EventTrackingSelector.PasteNode }, userActions.paste(event)) : null,
53-
cut: (event) =>
54-
userActions.cut ? eventWithStatistics({ selector: EventTrackingSelector.CutNode }, userActions.cut(event)) : null,
49+
copy: (event) => {
50+
if (isInputEvent(event)) return;
51+
userActions.copy ? eventWithStatistics({ selector: EventTrackingSelector.CopyNode }, userActions.copy(event)) : null;
52+
},
53+
paste: (event) => {
54+
userActions.paste ? eventWithStatistics({ selector: EventTrackingSelector.PasteNode }, userActions.paste(event)) : null;
55+
},
56+
cut: (event) => {
57+
if (isInputEvent(event)) return;
58+
userActions.cut ? eventWithStatistics({ selector: EventTrackingSelector.CutNode }, userActions.cut(event)) : null;
59+
},
5560
}),
5661
[eventWithStatistics, keyHandlers, userActions],
5762
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`GraphUtils prepareNewNodesWithLayout should update union output expression parameter with an updated node name when new unique node ids created 1`] = `
4+
{
5+
"idMapping": {
6+
"union": "union (copy 1)",
7+
"variable 1": "variable 1 (copy 1)",
8+
"variable 2": "variable 2 (copy 1)",
9+
},
10+
"layout": [
11+
{
12+
"id": "variable 1 (copy 1)",
13+
"position": {
14+
"x": 350,
15+
"y": 859,
16+
},
17+
},
18+
{
19+
"id": "variable 2 (copy 1)",
20+
"position": {
21+
"x": 710,
22+
"y": 859,
23+
},
24+
},
25+
{
26+
"id": "union (copy 1)",
27+
"position": {
28+
"x": 530,
29+
"y": 1039,
30+
},
31+
},
32+
],
33+
"nodes": [
34+
{
35+
"additionalFields": {
36+
"description": null,
37+
"layoutData": {
38+
"x": 0,
39+
"y": 720,
40+
},
41+
},
42+
"branchParameters": undefined,
43+
"id": "variable 1 (copy 1)",
44+
"type": "Variable",
45+
"value": {
46+
"expression": "'value'",
47+
"language": "spel",
48+
},
49+
"varName": "varName1",
50+
},
51+
{
52+
"additionalFields": {
53+
"description": null,
54+
"layoutData": {
55+
"x": 360,
56+
"y": 720,
57+
},
58+
},
59+
"branchParameters": undefined,
60+
"id": "variable 2 (copy 1)",
61+
"type": "Variable",
62+
"value": {
63+
"expression": "'value'",
64+
"language": "spel",
65+
},
66+
"varName": "varName2",
67+
},
68+
{
69+
"additionalFields": {
70+
"description": null,
71+
"layoutData": {
72+
"x": 180,
73+
"y": 900,
74+
},
75+
},
76+
"branchParameters": [
77+
{
78+
"branchId": "variable 1 (copy 1)",
79+
"parameters": [
80+
{
81+
"expression": {
82+
"expression": "1",
83+
"language": "spel",
84+
},
85+
"name": "Output expression",
86+
},
87+
],
88+
},
89+
{
90+
"branchId": "variable 2 (copy 1)",
91+
"parameters": [
92+
{
93+
"expression": {
94+
"expression": "2",
95+
"language": "spel",
96+
},
97+
"name": "Output expression",
98+
},
99+
],
100+
},
101+
],
102+
"id": "union (copy 1)",
103+
"nodeType": "union",
104+
"outputVar": "outputVar",
105+
"parameters": [],
106+
"type": "Join",
107+
},
108+
],
109+
}
110+
`;

0 commit comments

Comments
 (0)