Skip to content

Commit 3a8e8b5

Browse files
authored
Merge pull request #113 from netcreateorg/dev-bl/duplicate-node
Fixes: Gracefully handle duplicate Node labels
2 parents 21e69db + 49cadb9 commit 3a8e8b5

File tree

10 files changed

+261
-111
lines changed

10 files changed

+261
-111
lines changed

app-templates/_default.template.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ filterReduce = "Reduce"
1313
filterReduceHelp = "Show matches, Reduce (remove) others & recalculate sizes"
1414
filterFocus = "Focus"
1515
filterFocusHelp = "Show only nodes connected to the selected node within range"
16-
duplicateWarning = "You’re entering a duplicate node. Do you want to View the Existing node, or Continue creating?"
16+
duplicateWarning = "NOTE: At least one other node has the same name. Use search or the node table to check the others."
1717
nodeIsLockedMessage = "This node is currently being edited by someone else, please try again later."
1818
edgeIsLockedMessage = "This edge is currently being edited by someone else, please try again later."
1919
templateIsLockedMessage = "The template is currently being edited, please try again later."

app/view/netcreate/components/NCAutoSuggest.css

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,23 @@
1010
.matchlist div {
1111
padding: 0 10px;
1212
}
13+
14+
/* line hover is handled via javascript
15+
to support highlighting the current
16+
node. Do not use this css hover.
1317
.matchlist div:hover {
1418
background-color: #ddd;
1519
cursor: pointer;
16-
}
20+
}
21+
*/
22+
1723
.matchlist div.highlighted {
1824
background-color: #ccc;
1925
cursor: pointer;
2026
}
21-
27+
.matchlist .id {
28+
color: #3334;
29+
}
2230
.helptop {
2331
position: absolute;
2432
bottom: 120%;
@@ -27,3 +35,19 @@
2735
background-color: #ff0c;
2836
font-size: 12px;
2937
}
38+
39+
/* NC Node */
40+
.nodelabel > .matchlist {
41+
max-width: 300px;
42+
box-shadow: 4px 4px 6px 1px rgba(0, 0, 0, 0.1);
43+
margin-top: -10px;
44+
}
45+
.nodelabel > .matchlist .warning {
46+
line-height: 14px;
47+
padding-top: 4px;
48+
padding-bottom: 8px;
49+
}
50+
.nodelabel > .matchlist div:hover {
51+
background-color: transparent;
52+
cursor: default;
53+
}

app/view/netcreate/components/NCAutoSuggest.jsx

Lines changed: 138 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
USE:
66
77
<NCAutoSuggest
8-
statekey={key}
8+
parentKey={key}
99
value={value}
1010
onChange={this.handleInputUpdate}
1111
onSelect={this.handleSelection}
@@ -14,20 +14,28 @@
1414
PROPS
1515
1616
onChange(key, value) -- returns `key` and `value` for the input field
17-
onSelect(key, valuem, id) -- returns `key` and `value` for the final submission
18-
as well as the matching id
17+
18+
onSelect(parentKey, value, id)
19+
-- returns `state` and `value` for the final submission as well as the
20+
matching id. `value` is then passed back to NCAutoSuggest as the
21+
search field input value.
1922
2023
This will look up matching nodes via FIND_MATCHING_NODES nc-logic request.
2124
2225
This is a simple HTML component that will allow users to enter arbitrary
2326
text input. Any partial node labels will display as a list of popup
2427
menu options.
2528
26-
It can be used in a NCNode or NCEdge
29+
It can be used in a NCSearch or NCEdge
30+
(NOTE NCNode does not not use NCAutoSuggest, but displays a matchlist using
31+
a mechanism similar to NCAutoSuggest -- the key difference is that NCNode's
32+
matchlist is simply a static display list to let you know which nodes match
33+
the current input field, and does NOT support selecting a match.)
2734
28-
`statekey` provides a unique key for source/target selection
35+
`parentKey` provides a unique key to determine whether this NCAutoSuggest
36+
component is being used for a `search`, a `source`, or a `target` selection
2937
30-
Replaces the AutoComplete and AutoSuggest components.
38+
Replaces the deprecated AutoComplete and AutoSuggest components.
3139
3240
\*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * //////////////////////////////////////*/
3341

@@ -51,12 +59,19 @@ class NCAutoSuggest extends UNISYS.Component {
5159
this.state = {
5260
matches: [], // {id, label}
5361
higlightedLine: -1,
54-
isValidNode: true
62+
isValidNode: true,
63+
uShowMatchlist: false
5564
};
5665

66+
this.m_UIInputFocus = this.m_UIInputFocus.bind(this);
67+
this.m_UIInputClick = this.m_UIInputClick.bind(this);
5768
this.m_UIUpdate = this.m_UIUpdate.bind(this);
58-
this.m_UISelect = this.m_UISelect.bind(this);
69+
this.m_UISelectByLabel = this.m_UISelectByLabel.bind(this);
70+
this.m_UISelectById = this.m_UISelectById.bind(this);
5971
this.m_UIKeyDown = this.m_UIKeyDown.bind(this);
72+
this.m_UIMouseHighlightLine = this.m_UIMouseHighlightLine.bind(this);
73+
this.m_UIMouseUnhighlightLine = this.m_UIMouseUnhighlightLine.bind(this);
74+
this.m_UIHighlightLine = this.m_UIHighlightLine.bind(this);
6075
this.m_UIClickOutside = this.m_UIClickOutside.bind(this);
6176

6277
document.addEventListener('click', this.m_UIClickOutside);
@@ -70,6 +85,27 @@ class NCAutoSuggest extends UNISYS.Component {
7085
}
7186

7287
/**
88+
* User has clicked inside the input field to set selection point
89+
* This is needed to restore the selection point after a blur
90+
* `focus` fires before `click`
91+
*/
92+
m_UIInputFocus(event) {
93+
event.target.select();
94+
this.m_UIUpdate(event);
95+
}
96+
97+
/**
98+
* User has clicked in the input field, so update and show the matchlist
99+
* and catch the event to prevent the document click handler from firing other actions
100+
*/
101+
m_UIInputClick(event) {
102+
event.preventDefault(); // this prevents the document click handler from closing the matchlist
103+
event.stopPropagation();
104+
this.setState({ uShowMatchlist: true });
105+
}
106+
107+
/**
108+
* User has typed in the input field, or the field is getting focus again.
73109
* This processes the form data before passing it on to the parent handler.
74110
* The callback function is generally an input state update method in
75111
* NCNode or NCEdge
@@ -79,7 +115,6 @@ class NCAutoSuggest extends UNISYS.Component {
79115
const { onChange } = this.props;
80116
const key = event.target.id;
81117
const value = event.target.value;
82-
83118
// save the selection cursor position
84119
const selstart = event.target.selectionStart;
85120
const inputEl = event.target;
@@ -93,7 +128,7 @@ class NCAutoSuggest extends UNISYS.Component {
93128
return { id: d.id, label: d.label };
94129
})
95130
: undefined;
96-
this.setState({ matches, isValidNode });
131+
this.setState({ matches, isValidNode, uShowMatchlist: true });
97132
if (typeof onChange === 'function')
98133
onChange(key, value, () => {
99134
// restore selection cursor position
@@ -103,41 +138,78 @@ class NCAutoSuggest extends UNISYS.Component {
103138
});
104139
}
105140
/**
106-
* User has clicked an item in the matchlist, selecting one of the autosuggest items
141+
* User has clicked an item in the matchlist,
142+
* or selected an item by typing ENTER
143+
* selecting one of the autosuggest items
107144
* @param {Object} event
108-
* @param {string} key Usually either `source` or `target`
109-
* @param {string} value
145+
* @param {string} parentKey Either `search`, `source` or `target`
146+
* @param {string} value The autosuggest input value
110147
*/
111-
m_UISelect(event, key, value) {
148+
m_UISelectByLabel(event, parentKey, value) {
112149
event.preventDefault(); // catch click to close matchlist
113150
event.stopPropagation();
114151
const { onSelect } = this.props;
115152
const { matches } = this.state;
116-
const matchedNode = matches ? matches.find(n => n.label === value) : undefined;
117-
this.setState({ isValidNode: matchedNode, matches: [], higlightedLine: -1 }); // clear matches
118-
if (typeof onSelect === 'function')
119-
onSelect(key, value, matchedNode ? matchedNode.id : undefined); // callback function NCEdge.uiSourceTargetInputUpdate
153+
const matchedNodeViaID = matches ? matches.find(n => n.id === value) : undefined;
154+
this.setState({
155+
isValidNode: matchedNodeViaID,
156+
matches: [],
157+
higlightedLine: -1,
158+
uShowMatchlist: false
159+
}); // clear matches
160+
if (typeof onSelect === 'function') {
161+
onSelect(
162+
parentKey,
163+
value,
164+
matchedNodeViaID ? matchedNodeViaID.id : undefined // ...or id, not both
165+
); // callback function NCEdge.uiSourceTargetInputUpdate
166+
}
167+
}
168+
169+
m_UISelectById(event, parentKey, id) {
170+
event.preventDefault(); // catch click to close matchlist
171+
event.stopPropagation();
172+
const { onSelect, value } = this.props;
173+
const { matches } = this.state;
174+
const matchedNodeViaID = matches ? matches.find(n => n.id === id) : undefined;
175+
this.setState({
176+
isValidNode: matchedNodeViaID,
177+
matches: [],
178+
higlightedLine: -1,
179+
uShowMatchlist: false
180+
}); // clear matches
181+
if (typeof onSelect === 'function') {
182+
onSelect(
183+
parentKey,
184+
value, // show the current input field value
185+
matchedNodeViaID ? matchedNodeViaID.id : undefined // ...or `id`, not both
186+
); // callback function NCEdge.uiSourceTargetInputUpdate
187+
}
120188
}
189+
121190
/**
122191
* Handle key strokes
123-
* -- Hitting up/down arrow will select the higlight
124-
* -- Hitting Esc will cancel the autosuggest, also hitting Tab will prevent selecting the next field
125-
* -- Hitting Enter will select the item
192+
* -- Typing UP/DOWN arrow will select the higlight
193+
* -- Typing ESC will cancel the autosuggest, also hitting Tab will prevent selecting the next field
194+
* -- Typing ENTER will select the item
126195
* @param {Object} event
127196
*/
128197
m_UIKeyDown(event) {
129198
const { matches, higlightedLine } = this.state;
130-
const { statekey, value, onSelect } = this.props;
199+
const { parentKey, value, onSelect } = this.props;
131200
const keystroke = event.key;
132201
const lastLine = matches ? matches.length : -1;
133202
let newHighlightedLine = higlightedLine;
134203
if (keystroke === 'Enter') {
135-
let selectedValue = value;
136-
if (higlightedLine > -1) {
137-
// there is highlight, so select that
138-
selectedValue = matches[higlightedLine].label;
204+
if (higlightedLine > -1 && matches) {
205+
// make sure matches exists, b/c hitting Enter with a typo can end up with bad match
206+
// there is highlight, so select that using the id in the matchlist
207+
const id = matches[higlightedLine].id;
208+
this.m_UISelectById(event, parentKey, id); // user selects current highlight
209+
} else if (value !== '') {
210+
// Create a new node -- see also NCSearch
211+
this.m_UISelectByLabel(event, parentKey, value); // user selects current highlight
139212
}
140-
this.m_UISelect(event, statekey, selectedValue); // user selects current highlight
141213
}
142214
if (keystroke === 'Escape' || keystroke === 'Tab') {
143215
event.preventDefault(); // prevent tab key from going to the next field
@@ -151,52 +223,75 @@ class NCAutoSuggest extends UNISYS.Component {
151223
newHighlightedLine > -1 &&
152224
lastLine > 0
153225
) {
154-
newHighlightedLine = Math.min(lastLine - 1, Math.max(0, newHighlightedLine));
155-
this.setState({ higlightedLine: newHighlightedLine });
156-
const highlightedNode = matches[newHighlightedLine];
157-
UDATA.LocalCall('AUTOSUGGEST_HILITE_NODE', { nodeId: highlightedNode.id });
226+
this.m_UIHighlightLine(newHighlightedLine);
158227
}
159228
}
160229

230+
m_UIMouseHighlightLine(event, line) {
231+
this.m_UIHighlightLine(line);
232+
}
233+
m_UIMouseUnhighlightLine(event) {
234+
// Placeholder for future functionality
235+
// Catch the event, but don't do anything.
236+
// We want to keep the matchlist open even if you move the mouse
237+
// outside of the line.
238+
}
239+
240+
m_UIHighlightLine(line) {
241+
const { matches } = this.state;
242+
const lastLine = matches ? matches.length : -1;
243+
line = Math.min(lastLine - 1, Math.max(0, line));
244+
this.setState({ higlightedLine: line, uShowMatchlist: true });
245+
const highlightedNode = matches[line];
246+
UDATA.LocalCall('AUTOSUGGEST_HILITE_NODE', { nodeId: highlightedNode.id });
247+
}
248+
249+
// Clicking outside of the matchlist should close the autosuggest
161250
m_UIClickOutside(event) {
162-
if (!event.defaultPrevented) {
163-
this.setState({ matches: [], higlightedLine: -1 }); // close autosuggest
164-
}
251+
if (event.defaultPrevented)
252+
return; // clicking on the input field or the matchlist catches the click and prevents inadvertently closing the matchlist
253+
else this.setState({ matches: [], higlightedLine: -1, uShowMatchlist: false }); // close matchlist
165254
}
166255

167256
render() {
168-
const { matches, higlightedLine, isValidNode } = this.state;
169-
const { statekey, value, onSelect } = this.props;
257+
const { matches, higlightedLine, isValidNode, uShowMatchlist } = this.state;
258+
const { parentKey, value, onSelect } = this.props;
170259
const matchList =
171260
matches && matches.length > 0
172261
? matches.map((n, i) => (
173262
<div
174263
key={`${n.label}${i}`}
175264
value={n.label}
176265
className={higlightedLine === i ? 'highlighted' : ''}
177-
onClick={event => this.m_UISelect(event, statekey, n.label)}
266+
onClick={event => this.m_UISelectById(event, parentKey, n.id)}
267+
onMouseEnter={event => this.m_UIMouseHighlightLine(event, i)}
178268
>
179-
{n.label}
269+
{n.label} <span className="id">#{n.id}</span>
180270
</div>
181271
))
182272
: undefined;
183273
return (
184274
<div style={{ position: 'relative', flexGrow: '1' }}>
185275
<div className="helptop">Click on a node, or type a node name</div>
186276
<input
187-
id={statekey}
188-
key={`${statekey}input`}
277+
id={parentKey}
278+
key={`${parentKey}input`}
189279
value={value}
190280
type="string"
191281
className={!isValidNode ? 'invalid' : ''}
192282
onChange={this.m_UIUpdate}
193283
onKeyDown={this.m_UIKeyDown}
194-
onFocus={e => e.target.select()}
284+
onFocus={this.m_UIInputFocus}
285+
onClick={this.m_UIInputClick}
195286
autoComplete="off" // turn off Chrome's default autocomplete, which conflicts
196287
/>
197288
<br />
198-
{matchList && (
199-
<div id="matchlist" className="matchlist">
289+
{uShowMatchlist && matchList && (
290+
<div
291+
id="matchlist"
292+
className="matchlist"
293+
onMouseLeave={this.m_UIMouseUnhighlightLine}
294+
>
200295
{matchList}
201296
</div>
202297
)}

0 commit comments

Comments
 (0)