Skip to content

Commit 99a74af

Browse files
committed
Updated patient selection to be a dropdown list populated by patients in the current FHIR server
1 parent 6a0a974 commit 99a74af

File tree

5 files changed

+180
-26
lines changed

5 files changed

+180
-26
lines changed

src/components/PatientEntry/patient-entry.jsx

+33-10
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import Spacer from 'terra-spacer';
99
import Text from 'terra-text';
1010

1111
import styles from './patient-entry.css';
12-
import BaseEntryBody from '../BaseEntryBody/base-entry-body';
12+
import PatientSelect from '../PatientSelect/patient-select';
1313
import retrievePatient from '../../retrieve-data-helpers/patient-retrieval';
14+
import retrieveAllPatientIds from '../../retrieve-data-helpers/all-patient-retrieval';
1415

1516
const propTypes = {
1617
/**
@@ -63,6 +64,14 @@ export class PatientEntry extends Component {
6364
* Error message to display on the Field
6465
*/
6566
errorMessage: '',
67+
/**
68+
* The ID of the current Patient resource in context
69+
*/
70+
currentPatient: this.props.currentPatientId,
71+
/**
72+
* The list of the Patient identifiers populated from the currentFhirServer
73+
*/
74+
patients: [],
6675
};
6776

6877
this.handleCloseModal = this.handleCloseModal.bind(this);
@@ -77,13 +86,26 @@ export class PatientEntry extends Component {
7786
return null;
7887
}
7988

89+
async componentDidMount() {
90+
try {
91+
const data = await retrieveAllPatientIds();
92+
const patients = [];
93+
data.forEach((patient) => patients.push({ value: patient, label: patient }));
94+
this.setState({ patients: patients });
95+
} catch (error) {
96+
this.setState({ shouldDisplayError: true, errorMessage: 'Error fetching patients from FHIR Server' });
97+
return;
98+
}
99+
}
100+
80101
handleCloseModal() {
81102
this.setState({ isOpen: false, shouldDisplayError: false, errorMessage: '' });
82103
if (this.props.closePrompt) { this.props.closePrompt(); }
83104
}
84105

85106
handleChange(e) {
86-
this.setState({ userInput: e.target.value });
107+
this.setState({ userInput: e.value });
108+
this.setState({ currentPatient: e.value });
87109
}
88110

89111
async handleSubmit() {
@@ -137,14 +159,15 @@ export class PatientEntry extends Component {
137159
footer={footerContainer}
138160
onClose={this.props.isEntryRequired ? null : this.handleCloseModal}
139161
>
140-
<BaseEntryBody
141-
currentFhirServer={this.props.currentFhirServer}
142-
formFieldLabel="Enter a Patient ID"
143-
shouldDisplayError={this.state.shouldDisplayError}
144-
errorMessage={this.state.errorMessage}
145-
placeholderText={this.props.currentPatientId}
146-
inputOnChange={this.handleChange}
147-
inputName="patient-input"
162+
<PatientSelect
163+
currentFhirServer={this.props.currentFhirServer}
164+
formFieldLabel="Select a Patient"
165+
shouldDisplayError={this.state.shouldDisplayError}
166+
errorMessage={this.state.errorMessage}
167+
placeholderText={this.state.currentPatient}
168+
inputOnChange={this.handleChange}
169+
inputName="patient-input"
170+
patients={this.state.patients}
148171
/>
149172
</Dialog>
150173
</Modal>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.container {
2+
height: 100%;
3+
word-wrap: break-word;
4+
}
5+
6+
.vertical-separation {
7+
padding-top: 20px;
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import Text from 'terra-text';
4+
import Field from 'terra-form-field';
5+
import Select from 'react-select';
6+
7+
import styles from './patient-select.css';
8+
9+
const propTypes = {
10+
/**
11+
* If the modal needs to present the current FHIR server at the top, pass this prop in
12+
*/
13+
currentFhirServer: PropTypes.string,
14+
/**
15+
* The field label for the Field component (i.e. "Change Patient")
16+
*/
17+
formFieldLabel: PropTypes.string.isRequired,
18+
/**
19+
* A boolean flag to display an error if needed on the Field component
20+
*/
21+
shouldDisplayError: PropTypes.bool.isRequired,
22+
/**
23+
* If a error needs to be displayed in the Field component, accompany it with a message
24+
*/
25+
errorMessage: PropTypes.string,
26+
/**
27+
* If the Input component needs placeholder text (usually to help the user with example values), pass this prop in
28+
*/
29+
placeholderText: PropTypes.string,
30+
/**
31+
* If the value in the Input component changes (i.e user selects option), pass in a function callback to handle the text
32+
*/
33+
inputOnChange: PropTypes.func.isRequired,
34+
/**
35+
* The name attribute for the Input component
36+
*/
37+
inputName: PropTypes.string,
38+
/**
39+
* A list of the Patient identifiers that populate the select options
40+
*/
41+
patients: PropTypes.array.isRequired
42+
};
43+
44+
/**
45+
* PatientSelect (functional component) serves as the base UI inside modal interactions like "Change Patient".
46+
* It contains a Field for selecting an associated input (i.e. "Select a Patient"), and an Input for
47+
* allowing users to input text below its associated Field. Additionally, if relevant, the modal may present Text at the top which
48+
* displays the current FHIR server in context (useful for "Select a Patient" modals).
49+
*
50+
* How to use: Use this component if a modal needs to have some base UI for allowing a user to select an option, given some
51+
* Field text (i.e. "Select a Patient")
52+
*
53+
*/
54+
const PatientSelect = ({
55+
currentFhirServer, formFieldLabel, shouldDisplayError,
56+
errorMessage, placeholderText, inputOnChange, inputName,
57+
patients,
58+
}) => {
59+
let fhirServerDisplay;
60+
if (currentFhirServer) {
61+
fhirServerDisplay = (
62+
<div>
63+
<Text weight={400} fontSize={16}>Current FHIR server</Text>
64+
<br />
65+
<Text weight={200} fontSize={14}>{currentFhirServer}</Text>
66+
</div>
67+
);
68+
}
69+
70+
return (
71+
<div className={styles.container}>
72+
{fhirServerDisplay}
73+
<div className={styles['vertical-separation']}>
74+
<Field
75+
label={formFieldLabel}
76+
isInvalid={shouldDisplayError}
77+
error={errorMessage}
78+
required
79+
>
80+
<Select
81+
placeholder={placeholderText}
82+
value={placeholderText}
83+
onChange={inputOnChange}
84+
options={patients}
85+
/>
86+
</Field>
87+
</div>
88+
</div>
89+
);
90+
};
91+
92+
PatientSelect.propTypes = propTypes;
93+
94+
export default PatientSelect;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import axios from 'axios';
2+
import store from '../store/store';
3+
4+
function retrieveAllPatientIds() {
5+
return new Promise((resolve, reject) => {
6+
const { accessToken } = store.getState().fhirServerState;
7+
const fhirServer = store.getState().fhirServerState.currentFhirServer;
8+
const headers = {
9+
Accept: 'application/json+fhir',
10+
};
11+
const patientIds = [];
12+
13+
if (accessToken) {
14+
headers.Authorization = `Bearer ${accessToken.access_token}`;
15+
}
16+
17+
axios({
18+
method: 'get',
19+
url: `${fhirServer}/Patient`,
20+
headers,
21+
}).then((result) => {
22+
if (result.data && result.data.resourceType === 'Bundle'
23+
&& Array.isArray(result.data.entry) && result.data.entry.length) {
24+
for (const patient of result.data.entry) {
25+
patientIds.push(patient.resource.id);
26+
}
27+
return resolve(patientIds);
28+
} else {
29+
return reject();
30+
}
31+
}).catch((err) => {
32+
console.error('Could not retrieve patients from current FHIR server', err);
33+
return reject(err);
34+
});
35+
});
36+
}
37+
38+
export default retrieveAllPatientIds;

tests/components/PatientEntry/patient-entry.test.js

+7-16
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ describe('PatientEntry component', () => {
2626
PatientEntryView = require('../../../src/components/PatientEntry/patient-entry')['PatientEntry'];
2727
let component;
2828
if (mockResolve && mockClosePrompt) {
29-
component = <ConnectedView store={mockStore}
29+
component = <ConnectedView store={mockStore}
3030
resolve={mockResolve}
3131
isOpen={true}
32-
isEntryRequired={isEntryRequired}
32+
isEntryRequired={isEntryRequired}
3333
closePrompt={mockClosePrompt} />
3434
} else {
3535
component = <ConnectedView store={mockStore}/>;
@@ -41,9 +41,10 @@ describe('PatientEntry component', () => {
4141
beforeEach(() => {
4242
storeState = {
4343
patientState: {
44-
currentPatient: { id: 'test-patient' }
44+
currentPatient: { id: 'test-1' }
4545
},
46-
fhirServerState: { currentFhirServer: 'http://test-fhir.com' }
46+
fhirServerState: { currentFhirServer: 'http://test-fhir.com' },
47+
patients: ["test-1", "test-2", "test-3"],
4748
};
4849
mockSpy = jest.fn();
4950
mockResolve = jest.fn();
@@ -86,7 +87,8 @@ describe('PatientEntry component', () => {
8687

8788
describe('User input', () => {
8889
const enterInputAndSave = (shallowedComponent, input) => {
89-
shallowedComponent.find('BaseEntryBody').dive().find('Input').simulate('change', {'target': {'value': input}});
90+
shallowedComponent.find('PatientSelect').simulate('change', {'value': input});
91+
let x = shallowedComponent.find('PatientSelect');
9092
shallowedComponent.find('Dialog').dive().find('ContentContainer').dive().find('.right-align').find('Button').at(0).simulate('click');
9193
};
9294

@@ -106,16 +108,5 @@ describe('PatientEntry component', () => {
106108
expect(shallowedComponent.state('shouldDisplayError')).toEqual(true);
107109
expect(shallowedComponent.state('errorMessage')).not.toEqual('');
108110
});
109-
110-
it('closes the modal, resolves passed in prop promise if applicable, and closes prompt if possible', async () => {
111-
mockSpy = jest.fn(() => { return Promise.resolve(1)} );
112-
setup(storeState);
113-
let shallowedComponent = pureComponent.shallow();
114-
await enterInputAndSave(shallowedComponent, 'test');
115-
expect(shallowedComponent.state('shouldDisplayError')).toEqual(false);
116-
expect(mockClosePrompt).toHaveBeenCalled();
117-
expect(mockResolve).toHaveBeenCalled();
118-
expect(mockClosePrompt).toHaveBeenCalled();
119-
});
120111
});
121112
});

0 commit comments

Comments
 (0)