Skip to content

Commit

Permalink
Merge pull request #87 from Bahmni/snomed-master
Browse files Browse the repository at this point in the history
Merge Form Controls to Master for Building NPM Package
  • Loading branch information
sivareddyp committed Jul 12, 2023
2 parents b7b81e1 + b02029a commit b75ee0b
Show file tree
Hide file tree
Showing 10 changed files with 1,052 additions and 155 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
Form Controls
-------------

This library provides a range of form controls that can be used to create customized forms within the Bahmni platform.

### File naming conventions

1. All components should be in Pascal Case (camel case starting with uppercase letter)
Expand All @@ -18,3 +20,7 @@ Form Controls
1. Install dependencies - `yarn`
2. Build - `yarn build`
3. Test - `yarn test`

### SNOMED Integration Support

form-controls also integrates with SNOMED for terminology lookup as part of form configuration and generation. More details can be found in [this](https://bahmni.atlassian.net/wiki/spaces/BAH/pages/3132686337/SNOMED+FHIR+Terminology+Server+Integration+with+Bahmni) Wiki link
63 changes: 43 additions & 20 deletions src/components/AutoComplete.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@ import { Validator } from 'src/helpers/Validator';
import isEmpty from 'lodash/isEmpty';
import isEqual from 'lodash/isEqual';
import classNames from 'classnames';
import { Util } from 'src/helpers/Util';


export class AutoComplete extends Component {
constructor(props) {
super(props);
this.optionsUrl = props.optionsUrl;
this.terminologyServiceConfig = props.terminologyServiceConfig;
this.childRef = undefined;
this.getValue = this.getValue.bind(this);
this.getOptions = this.getOptions.bind(this);
this.handleChange = this.handleChange.bind(this);
this.onInputChange = this.onInputChange.bind(this);
this.handleFocus = this.handleFocus.bind(this);
this.storeChildRef = this.storeChildRef.bind(this);
this.debouncedOnInputChange = Util.debounce(this.onInputChange, 300);
const errors = this._getErrors(props.value) || [];
const hasErrors = this._isCreateByAddMore() ? this._hasErrors(errors) : false;
this.state = {
Expand All @@ -32,7 +35,11 @@ export class AutoComplete extends Component {
}

componentWillMount() {
if (!this.props.asynchronous && this.props.minimumInput === 0) {
if (
!this.props.asynchronous &&
this.props.minimumInput === 0 &&
!this.props.url
) {
this.setState({ options: this.props.options });
}
}
Expand Down Expand Up @@ -75,26 +82,38 @@ export class AutoComplete extends Component {
}

onInputChange(input) {
if (input.length >= this.props.minimumInput) {
const options = this.props.options;
const searchedInputs = input.trim().split(' ');
const filteredOptions = [];
const { options, url } = this.props;
const { getAnswers, formatConcepts } = Util;

if (input.length < this.props.minimumInput) {
this.setState({ options: [], noResultsText: 'Type to search' });
return;
}

options.forEach(option => {
let flag = true;
searchedInputs.forEach(searchedInput => {
const regEx = new RegExp(searchedInput, 'gi');
flag = (flag && option.name.match(regEx));
if (url) {
getAnswers(url, input, this.terminologyServiceConfig.limit || 30)
.then(data => {
const responses = formatConcepts(data);
this.setState({
options: responses,
noResultsText: 'No Results Found',
});
})
.catch(() => {
this.setState({ options: [], noResultsText: 'No Results Found' });
});
if (flag) { filteredOptions.push(option); }
} else {
const searchedInputs = input.trim().split(' ');
const filteredOptions = options.filter(option =>
searchedInputs.every(searchedInput =>
option.name.match(new RegExp(searchedInput, 'gi'))
)
);
this.setState({
options: filteredOptions,
noResultsText: 'No Results Found',
});

this.setState({ options: filteredOptions });
this.setState({ noResultsText: 'No Results Found' });
return;
}
this.setState({ noResultsText: 'Type to search' });
this.setState({ options: [] });
}

getOptions(input = '') {
Expand All @@ -114,7 +133,8 @@ export class AutoComplete extends Component {

getValue() {
if (this.state.value) {
return this.props.multiSelect ? this.state.value : [this.state.value];
const value = this.props.multiSelect ? this.state.value : [this.state.value];
return value.map(val => ({ ...val, uuid: val.uuid || val.value }));
}
return [];
}
Expand Down Expand Up @@ -193,7 +213,7 @@ export class AutoComplete extends Component {
{ ...props }
filterOptions={ null }
noResultsText={this.state.noResultsText}
onInputChange={this.onInputChange}
onInputChange={this.debouncedOnInputChange}
options={ this.state.options }
ref={ this.storeChildRef }
/>
Expand All @@ -217,6 +237,8 @@ AutoComplete.propTypes = {
options: PropTypes.array,
optionsUrl: PropTypes.string,
searchable: PropTypes.bool,
terminologyServiceConfig: PropTypes.object,
url: PropTypes.string,
validateForm: PropTypes.bool,
validations: PropTypes.array,
value: PropTypes.any,
Expand All @@ -231,9 +253,10 @@ AutoComplete.defaultProps = {
enabled: true,
formFieldPath: '-0',
labelKey: 'display',
minimumInput: 2,
minimumInput: 3,
multiSelect: false,
optionsUrl: '/openmrs/ws/rest/v1/concept?v=full&q=',
url: '',
valueKey: 'uuid',
searchable: true,
};
Expand Down
126 changes: 103 additions & 23 deletions src/components/CodedControl.jsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,75 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ComponentStore from 'src/helpers/componentStore';
import map from 'lodash/map';
import find from 'lodash/find';
import each from 'lodash/each';
import { IntlShape } from 'react-intl';

import constants from 'src/constants';
import { Util } from '../helpers/Util';
export class CodedControl extends Component {
constructor(props) {
super(props);
this.onValueChange = this.onValueChange.bind(this);
this.state = {
codedData: this.props.options,
success: false,
terminologyServiceConfig: {
limit: 30,
},
};
}

componentDidMount() {
this.getAnswers();
}

onValueChange(value, errors, triggerControlEvent) {
const updatedValue = this._getUpdatedValue(value);
this.props.onChange({ value: updatedValue, errors, triggerControlEvent });
}

getAnswers() {
const { properties } = this.props;
const { getConfig, getAnswers, formatConcepts } = Util;

if (!properties.url) {
this.setState({ success: true });
return;
}

getConfig(properties.url)
.then(response => {
const { terminologyService } = response.config || {};

if (terminologyService) {
this.setState(prevState => ({
terminologyServiceConfig: {
...prevState.terminologyServiceConfig,
...terminologyService,
},
}));
}

if (properties.autoComplete) {
this.setState({ success: true });
return Promise.resolve([]);
}

return getAnswers(properties.url, '', this.state.terminologyServiceConfig.limit);
})
.then(data => {
if (!data) return;
const options = formatConcepts(data);
this.setState({ codedData: options, success: true });
})
.catch(() => {
this.props.showNotification(
'Something unexpected happened.',
constants.messageType.error
);
});
}

_getUpdatedValue(value) {
const multiSelect = this.props.properties.multiSelect;
if (value) {
Expand All @@ -28,38 +81,55 @@ export class CodedControl extends Component {

_getOptionsFromValues(values, multiSelect) {
const options = [];
each(values, (value) => {
options.push(find(this.props.options, ['uuid', value.value]));
if (this.props.properties.url) return multiSelect ? values : values[0];
each(values, value => {
options.push(find(this.state.codedData, ['uuid', value.value]));
});
return multiSelect ? options : options[0];
}

_getOptionsRepresentation(options) {
const optionsRepresentation = [];
map(options, (option) => {
return options.map(option => {
const message = {
id: option.translationKey || 'defaultId',
defaultMessage: option.name.display || option.name,
};
const formattedMessage = this.props.intl.formatMessage(message);
optionsRepresentation.push({ name: formattedMessage, value: option.uuid });
const result = {
name: formattedMessage,
value: option.uuid,
};
if (this.props.properties.url) {
result.codedAnswer = option.codedAnswer;
result.uuid = option.uuid || option.value;
}
return result;
});
return optionsRepresentation;
}

_getValue(value, multiSelect) {
if (value) {
const updatedValue = multiSelect ? value : [value];
updatedValue.map((val) => {
const updatedVal = val;
const codedAnswer = find(this.props.options, (option) => option.uuid === val.uuid);
updatedVal.translationKey = codedAnswer ? codedAnswer.translationKey : '';
return updatedVal;
});
const options = this._getOptionsRepresentation(updatedValue, multiSelect);
return multiSelect ? options : options[0];
}
return undefined;
if (!value) return undefined;

const getMapping = val => (val.mappings && !this.props.properties.autoComplete ?
find(val.mappings, ['source', this.state.terminologyServiceConfig.source]) : null);

const codedAnswer = val => {
const mapping = getMapping(val);
if (mapping) {
return this.state.codedData.find(option => option.uuid.replace(/\D/g, '') === mapping.code);
}
return find(this.state.codedData, option => option.uuid === (val.uuid || val.value));
};

const createAnswer = val => {
const coded = codedAnswer(val);
return coded || { ...val, name: val.name, uuid: val.uuid || val.value, translationKey: '' };
};

const options = multiSelect ? value.map(createAnswer) : [createAnswer(value)];

const optionsRepresentation = this._getOptionsRepresentation(options, multiSelect);
return multiSelect ? optionsRepresentation : optionsRepresentation[0];
}

_getChildProps(displayType) {
Expand All @@ -77,11 +147,16 @@ export class CodedControl extends Component {
formFieldPath,
value: this._getValue(value, multiSelect),
onValueChange: this.onValueChange,
options: this._getOptionsRepresentation(this.props.options, multiSelect),
options: this._getOptionsRepresentation(
this.state.codedData,
multiSelect
),
validate,
validateForm,
validations,
multiSelect,
url: this.props.properties.url,
terminologyServiceConfig: this.state.terminologyServiceConfig,
};
if (displayType === 'autoComplete' || displayType === 'dropDown') {
props.asynchronous = false;
Expand All @@ -103,10 +178,14 @@ export class CodedControl extends Component {
render() {
const { properties } = this.props;
const displayType = this._getDisplayType(properties);
const registeredComponent = ComponentStore.getRegisteredComponent(displayType);
const registeredComponent =
ComponentStore.getRegisteredComponent(displayType);
if (registeredComponent) {
const childProps = this._getChildProps(displayType);
return React.createElement(registeredComponent, childProps);
return (
this.state.success &&
React.createElement(registeredComponent, childProps)
);
}
return null;
}
Expand All @@ -119,6 +198,7 @@ CodedControl.propTypes = {
onChange: PropTypes.func.isRequired,
options: PropTypes.array.isRequired,
properties: PropTypes.object.isRequired,
showNotification: PropTypes.func.isRequired,
validate: PropTypes.bool.isRequired,
validateForm: PropTypes.bool.isRequired,
validations: PropTypes.array.isRequired,
Expand Down
2 changes: 1 addition & 1 deletion src/components/ObsControl.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class ObsControl extends addMoreDecorator(Component) {

constructor(props) {
super(props);
this.state = { };
this.state = {};
this.onChange = this.onChange.bind(this);
this.onCommentChange = this.onCommentChange.bind(this);
this.onAddControl = this.onAddControl.bind(this);
Expand Down
Loading

0 comments on commit b75ee0b

Please sign in to comment.