Skip to content

Commit b356c44

Browse files
author
Matthew Gramigna
authored
Merge pull request #786 from FluxNotes/shortcut-entry-overhaul
gg
2 parents 32cd60f + dfed558 commit b356c44

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+1998
-1872
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
# dependencies
44
node_modules
5+
package-lock.json
56

67
# testing
78
/coverage
@@ -17,8 +18,11 @@ yarn-error.log*
1718

1819
# sass outputs
1920
# *.css // Add once project has completely converted to SASS
21+
/src/context/EditorPortal.css
2022
/src/context/ContextOptions.css
2123
/src/context/ContextItem.css
24+
/src/context/ContextListOptions.css
25+
/src/context/ContextGetHelp.css
2226
/src/context/ContextTray.css
2327
/src/context/ShortcutViewModeContent.css
2428
/src/context/SnippetViewModeContent.css

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@
124124
"test-backend:darwin:linux": "CI=true NO_PROXY=localhost react-app-rewired test --env=jsdom --colors",
125125
"test-fullapp": "run-script-os",
126126
"test-fullapp:win32": "set CI=true&&react-app-rewired test --env=jsdom test\\backend\\views\\FullApp.test.js",
127-
"test-fullapp:darwin:linux": "CI=true react-app-rewired test --env=jsdom test/backend/views/FullApp.test.js",
127+
"test-fullapp:darwin:linux": "CI=true react-app-rewired test --env=jsdom --colors __test__/backend/views/FullApp.test.js",
128128
"test-mcode": "run-script-os",
129129
"test-mcode:win32": "set CI=true&&react-app-rewired test --env=jsdom \\\\test\\\\backend\\\\mcode\\\\.*\\.test\\.js",
130130
"test-mcode:darwin:linux": "CI=true react-app-rewired test --env=jsdom test/backend/mcode/*.test.js",

src/__test__/backend/lib/toxicreaction_lookup.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ describe('getAttributionCodeableConcept', function() {
3535

3636

3737
it('should return CodeableConcept object with correct coding/codesystem when passed a value in the list.', function() {
38-
const goodValue = 'Disease';
38+
const goodValue = 'Illness';
3939
const codeableConcept = lookup.getAttributionCodeableConcept(goodValue);
4040

4141
expect(codeableConcept)

src/__test__/backend/views/FullApp.test.js

+331-28
Large diffs are not rendered by default.

src/context/Context.jsx

+18-4
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import Lang from 'lodash';
33
export default class Context {
44
constructor() {
55
this.children = [];
6+
this.isObjectComplete = false;
67
this._initialContextPosition = -1; // Where to insert the context relative to the others on creation
78
}
89

910
initialize(contextManager, trigger = undefined, updatePatient = true) {
1011
this.contextManager = contextManager;
11-
this.isInContext = false;
12+
this.isInContext = this.isInContext || false;
1213
}
1314

1415
getId() {
@@ -41,8 +42,9 @@ export default class Context {
4142
return this.children;
4243
}
4344

44-
getLabel() {
45-
throw new Error("Invalid context. " + this.constructor.name);
45+
removeParent() {
46+
this.parentContext = undefined;
47+
this.isObjectComplete = false;
4648
}
4749

4850
getValueObject() {
@@ -100,13 +102,25 @@ export default class Context {
100102
}
101103
}
102104

105+
/**
106+
* Get the key
107+
* @returns Unique key
108+
*/
103109
getKey() {
104110
return this.key;
105111
}
106112

113+
/**
114+
* Sets the key
115+
* The field is a unique key generated by Slate assigned to all Context instances to be used by FNE and SFP
116+
* @param key
117+
*/
107118
setKey(key) {
108119
this.key = key;
109-
if (this.isContext() && this.contextManager) {
120+
// Shortcuts that require selection from options and are not completed are not in context.
121+
// Shortcuts that don't require selection from options and are not completed are still added.
122+
const shouldAddToContext = (this.needToSelectValueFromMultipleOptions() && this.isComplete) || !this.needToSelectValueFromMultipleOptions();
123+
if (this.isContext() && this.contextManager && shouldAddToContext) {
110124
this.contextManager.addShortcutToContext(this);
111125
this.isInContext = true;
112126
}

src/context/ContextCalendar.jsx

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import Calendar from 'rc-calendar';
4+
5+
class ContextCalendar extends React.Component {
6+
handleDateSelect = (date) => {
7+
this.props.closePortal();
8+
const context = { key: 'set-date-id', context: `${date.format('D MMM YYYY')}`, object: date };
9+
this.props.onSelected(this.props.state, context);
10+
}
11+
12+
render() {
13+
return (
14+
<Calendar
15+
showDateInput={false}
16+
onSelect={this.handleDateSelect}
17+
ref={input => input && setTimeout(() => { input.focus(); }, 100)}
18+
/>
19+
);
20+
}
21+
}
22+
23+
ContextCalendar.propTypes = {
24+
closePortal: PropTypes.func.isRequired,
25+
onSelected: PropTypes.func.isRequired,
26+
state: PropTypes.object,
27+
};
28+
29+
export default ContextCalendar;

src/context/ContextGetHelp.jsx

+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import './ContextGetHelp.css';
4+
import NoteParser from '../noteparser/NoteParser';
5+
6+
7+
const UP_ARROW_KEY = 38;
8+
const DOWN_ARROW_KEY = 40;
9+
const ENTER_KEY = 13;
10+
11+
class ContextGetHelp extends React.Component {
12+
constructor(props) {
13+
super(props);
14+
this.noteParser = new NoteParser();
15+
16+
// eventually we can set this up to have custom options as a prop
17+
const defaultOptions = [
18+
{
19+
text: 'expand',
20+
onSelect: this.expand
21+
}
22+
];
23+
24+
this.state = {
25+
selectedIndex: -1,
26+
getHelpOptions: defaultOptions
27+
};
28+
}
29+
30+
componentWillUnmount() {
31+
this.props.closePortal();
32+
}
33+
34+
expand = () => {
35+
this.props.closePortal();
36+
const transform = this.replaceCurrentShortcut(this.props.shortcut.metadata.expandedText);
37+
return this.props.onSelected(transform.apply(), null);
38+
}
39+
40+
replaceCurrentShortcut = (selection) => {
41+
let transform;
42+
transform = this.props.state.transform();
43+
const triggers = this.noteParser.getListOfTriggersFromText(selection)[0];
44+
triggers.forEach((trigger, idx) => {
45+
if (idx !== 0) {
46+
transform = this.props.insertShortcut(trigger.definition, trigger.trigger, trigger.selectedValue, transform, 'typed');
47+
}
48+
if (idx < triggers.length-1) {
49+
transform = transform.insertText(selection.substring(trigger.endIndex, triggers[idx+1].startIndex));
50+
}
51+
else if (trigger.endIndex < selection.length) {
52+
transform = transform.insertText(selection.substring(trigger.endIndex));
53+
}
54+
});
55+
return transform;
56+
}
57+
58+
setSelectedIndex = (selectedIndex) => {
59+
this.setState({ selectedIndex });
60+
}
61+
62+
/*
63+
* Change the menu position based on the amount of places to move
64+
*/
65+
changeMenuPosition = (change) => {
66+
const optionsCount = this.state.getHelpOptions.length;
67+
let newSelectedIndex = this.state.selectedIndex;
68+
if ((change === -1 && this.state.selectedIndex > -1) || (change === 1 && this.state.selectedIndex < optionsCount)) {
69+
newSelectedIndex = this.state.selectedIndex + change;
70+
}
71+
// wrap back to top on down arrow of last option
72+
if (change === 1 && this.state.selectedIndex === optionsCount) {
73+
newSelectedIndex = 0;
74+
}
75+
this.setSelectedIndex(newSelectedIndex);
76+
}
77+
78+
/*
79+
* Navigate and interact with menu based on button presses
80+
*/
81+
onKeyDown = (e) => {
82+
const keyCode = e.which;
83+
if (keyCode === DOWN_ARROW_KEY || keyCode === UP_ARROW_KEY) {
84+
e.preventDefault();
85+
e.stopPropagation();
86+
const positionChange = (keyCode === DOWN_ARROW_KEY) ? 1 : -1;
87+
this.changeMenuPosition(positionChange);
88+
} else if (keyCode === ENTER_KEY) {
89+
// NOTE: This operations might not work on SyntheticEvents which are populat in react
90+
91+
// close portal if enter key is pressed but no dropdown option is in focus
92+
if (this.state.selectedIndex === -1) {
93+
e.preventDefault();
94+
e.stopPropagation();
95+
this.props.closePortal();
96+
}
97+
98+
// one of the get help options is selected via enter key
99+
else if (this.state.selectedIndex > 0) {
100+
e.preventDefault();
101+
e.stopPropagation();
102+
103+
// the parent 'get help' option is not included in the getHelpOptions array
104+
// but it is included as a selectedIndex, so there is an off by one that needs
105+
// to be calculated, hence the -1
106+
return this.state.getHelpOptions[this.state.selectedIndex-1].onSelect();
107+
}
108+
}
109+
}
110+
111+
renderOptions() {
112+
// if getHelp is not selected, don't show the additional options
113+
if (this.state.selectedIndex === -1) return null;
114+
115+
return (
116+
<span className="context-get-help-options">
117+
{this.state.getHelpOptions.map((option, index) => {
118+
// the parent 'get help' option is not included in the getHelpOptions array
119+
// but it is included as a selectedIndex, so there is an off by one that needs
120+
// to be calculated, hence the updatedIndex + 1 from the index of the getHelpOptions
121+
const updatedIndex = index + 1;
122+
return (
123+
<li key={updatedIndex}
124+
data-active={this.state.selectedIndex === updatedIndex}
125+
onClick={option.onSelect}
126+
onMouseEnter={() => { this.setSelectedIndex(updatedIndex); }}
127+
>
128+
{option.text}
129+
</li>
130+
);
131+
})}
132+
</span>
133+
);
134+
}
135+
136+
renderIsCompleteMessage() {
137+
const initiatingTrigger = this.props.shortcut.getDisplayText();
138+
return (
139+
<ul className="context-get-help" ref="contextGetHelp">
140+
<li
141+
className="context-get-help-li"
142+
>
143+
<span className="context-get-help-text">
144+
<i>{initiatingTrigger} is already complete</i>
145+
</span>
146+
</li>
147+
</ul>
148+
);
149+
}
150+
151+
renderIsMissingParent() {
152+
const initiatingTrigger = this.props.shortcut.getDisplayText();
153+
return (
154+
<ul className="context-get-help" ref="contextGetHelp">
155+
<li
156+
className="context-get-help-li"
157+
>
158+
<span className="context-get-help-text">
159+
<i>{initiatingTrigger} is missing a parent</i>
160+
</span>
161+
</li>
162+
</ul>
163+
);
164+
}
165+
166+
167+
168+
render() {
169+
// If the shortcut we're responsible for is missing a parent, display a message to the user to avoid confusion
170+
if (!this.props.shortcut.hasParentContext() && this.props.shortcut.hasChildren()) return this.renderIsMissingParent();
171+
// If the shortcut we're responsible for is complete, display a message to the user to avoid confusion
172+
if (this.props.shortcut.isComplete) return this.renderIsCompleteMessage();
173+
// Else we should display all our getHelp message
174+
const initiatingTrigger = this.props.shortcut.getDisplayText();
175+
let iconClass = 'fa fa-angle-';
176+
this.state.selectedIndex === -1 ? iconClass += 'down' : iconClass += 'up';
177+
return (
178+
<ul className="context-get-help" ref="contextGetHelp">
179+
<li
180+
className="context-get-help-li"
181+
data-active={this.state.selectedIndex === 0}
182+
onMouseEnter={() => { this.setSelectedIndex(0); }}
183+
>
184+
<span className="context-get-help-text">
185+
<i>get help with {initiatingTrigger}</i>
186+
<span className={iconClass}></span>
187+
</span>
188+
</li>
189+
{this.renderOptions()}
190+
</ul>
191+
);
192+
}
193+
}
194+
195+
ContextGetHelp.propTypes = {
196+
closePortal: PropTypes.func.isRequired,
197+
shortcut: PropTypes.object.isRequired,
198+
state: PropTypes.object.isRequired
199+
};
200+
201+
export default ContextGetHelp;

src/context/ContextGetHelp.scss

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
@import '../styles/variables';
2+
3+
ul.context-get-help {
4+
max-height: 250px;
5+
overflow-y: auto;
6+
overflow-x: hidden;
7+
list-style-type: none;
8+
padding: 0;
9+
margin: 0;
10+
background-color: $background;
11+
12+
li {
13+
padding: 5px 10px;
14+
display: flex;
15+
justify-content: space-between;
16+
& > * {
17+
display: block;
18+
}
19+
}
20+
21+
li[data-active="true"] {
22+
background-color: $interface-blue;
23+
color: $background;
24+
}
25+
26+
.context-get-help-li {
27+
margin-top: 5px;
28+
margin-bottom: 5px;
29+
30+
.context-get-help-text {
31+
span {
32+
padding-left: 15px;
33+
}
34+
}
35+
}
36+
37+
.context-get-help-options {
38+
li:first-child {
39+
border-top: 1px solid $line-gray;
40+
}
41+
42+
li:last-child {
43+
margin-bottom: 5px;
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)