Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge Form Controls to Master for Building NPM Package #87

Merged
merged 11 commits into from
Jul 12, 2023
Merged
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