Skip to content

Commit

Permalink
(feat) Introduce Location Datasource (#113)
Browse files Browse the repository at this point in the history
* Introduce concept location control

* Refactor datasource implementation code
  • Loading branch information
kajambiya authored Sep 18, 2023
1 parent 34f60bc commit fabd48d
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 21 deletions.
4 changes: 3 additions & 1 deletion src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ export interface OHRIFormQuestionOptions {
};
isDateTime?: { labelTrue: boolean; labelFalse: boolean };
usePreviousValueDisabled?: boolean;
datasource?: { id: string; config?: Record<string, any> };
isSearchable?: boolean;
}

export type SessionMode = 'edit' | 'enter' | 'view';
Expand Down Expand Up @@ -251,7 +253,7 @@ export interface DataSource<T> {
/**
* Fetches arbitrary data from a data source
*/
fetchData(searchTerm?: string): Promise<Array<T>>;
fetchData(searchTerm?: string, config?: Record<string, any>): Promise<Array<T>>;
/**
* Maps a data source item to an object with a uuid and display property
*/
Expand Down
44 changes: 26 additions & 18 deletions src/components/inputs/ui-select-extended/ui-select-extended.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,16 @@ export const UISelectExtended: React.FC<OHRIFormFieldProps> = ({ question, handl
const [errors, setErrors] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [displayValue, setDisplayValue] = useState('');
const isFieldRequiredError = useMemo(() => errors[0]?.errCode == fieldRequiredErrCode, [errors]);
const [previousValueForReview, setPreviousValueForReview] = useState(null);
const inputValue = useRef('');
const dataSource = useMemo(() => getDataSource(question.questionOptions['datasource']), []);
const [inputValue, setInputValue] = useState('');
const isProcessingSelection = useRef(false);

const [dataSource, config] = useMemo(
() => [getDataSource(question.questionOptions?.datasource?.id), question.questionOptions?.datasource?.config],
[],
);

useEffect(() => {
if (question['submission']) {
question['submission'].errors && setErrors(question['submission'].errors);
Expand All @@ -43,17 +48,17 @@ export const UISelectExtended: React.FC<OHRIFormFieldProps> = ({ question, handl

const debouncedSearch = debounce((searchterm, dataSource) => {
setIsLoading(true);
dataSource.fetchData(searchterm).then(dataItems => {
dataSource.fetchData(searchterm, config).then(dataItems => {
setItems(dataItems.map(dataSource.toUuidAndDisplay));
setIsLoading(false);
});
}, 300);

useEffect(() => {
// If not searchable, preload the items
if (dataSource && !isTrue(question.questionOptions['isSearchable'])) {
if (dataSource && !isTrue(question.questionOptions.isSearchable)) {
setIsLoading(true);
dataSource.fetchData().then(dataItems => {
dataSource.fetchData(null, config).then(dataItems => {
setItems(dataItems.map(dataSource.toUuidAndDisplay));
setIsLoading(false);
});
Expand All @@ -62,7 +67,7 @@ export const UISelectExtended: React.FC<OHRIFormFieldProps> = ({ question, handl

useEffect(() => {
// get the data source
if (dataSource && isTrue(question.questionOptions['isSearchable']) && !isEmpty(searchTerm)) {
if (dataSource && isTrue(question.questionOptions.isSearchable) && !isEmpty(searchTerm)) {
debouncedSearch(searchTerm, dataSource);
}
}, [dataSource, searchTerm]);
Expand All @@ -82,15 +87,7 @@ export const UISelectExtended: React.FC<OHRIFormFieldProps> = ({ question, handl
}
}, [encounterContext?.previousEncounter]);

const handleStateChange = (changes, stateAndHelpers) => {
// Intercept the state change for onBlur event
if (changes?.type === stateAndHelpers?.changeTypes?.blur && !changes?.selectedItem) {
// Return modified state to persist the inputValue
return { ...changes, value: inputValue.current };
}
return changes;
};
return encounterContext.sessionMode == 'view' ? (
return encounterContext.sessionMode == 'view' || isTrue(question.readonly) ? (
<div className={styles.formField}>
<OHRIFieldValueView
label={question.label}
Expand Down Expand Up @@ -128,11 +125,22 @@ export const UISelectExtended: React.FC<OHRIFormFieldProps> = ({ question, handl
}
return item.display.toLowerCase().includes(inputValue.toLowerCase());
}}
onChange={({ selectedItem }) => handleChange(selectedItem?.uuid)}
onChange={({ selectedItem }) => {
isProcessingSelection.current = true;
handleChange(selectedItem?.uuid);
}}
disabled={question.disabled}
readOnly={question.readonly}
onInputChange={value => {
inputValue.current = value;
if (isProcessingSelection.current) {
// Notes:
// When the user selects a value, both the onChange and onInputChange functions are invoked sequentially.
// Issue: onInputChange modifies the search term, unnecessarily triggering a search.
isProcessingSelection.current = false;
return;
}
setInputValue('');
setFieldValue(question.id, '');
if (question.questionOptions['isSearchable']) {
setSearchTerm(value);
}
Expand Down
23 changes: 23 additions & 0 deletions src/datasources/data-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { openmrsFetch, OpenmrsResource } from '@openmrs/esm-framework';
import { DataSource } from '../api/types';

export class BaseOpenMRSDataSource implements DataSource<OpenmrsResource> {
url: string;

constructor(url: string) {
this.url = url;
}

fetchData(searchTerm: string): Promise<any[]> {
return openmrsFetch(searchTerm ? `${this.url}&q=${searchTerm}` : this.url).then(({ data }) => {
return data.results;
});
}

toUuidAndDisplay(data: OpenmrsResource): OpenmrsResource {
if (typeof data.uuid === 'undefined' || typeof data.display === 'undefined') {
throw new Error("'uuid' or 'display' not found in the OpenMRS object.");
}
return data;
}
}
19 changes: 19 additions & 0 deletions src/datasources/location-data-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { openmrsFetch } from '@openmrs/esm-framework';
import { BaseOpenMRSDataSource } from './data-source';

export class LocationDataSource extends BaseOpenMRSDataSource {
constructor(){
super("/ws/rest/v1/location?v=custom:(uuid,display)");
}

fetchData(searchTerm: string, config?: Record<string, any>): Promise<any[]> {
if (config?.tag) {
let urlParts = this.url.split('?');
this.url = `${urlParts[0]}?tag=${config.tag}&${urlParts[1]}`;
}
return openmrsFetch(searchTerm ? `${this.url}&q=${searchTerm}` : this.url).then(({ data }) => {
return data.results;
});
}

}
10 changes: 8 additions & 2 deletions src/registry/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import { OHRIFormsStore } from '../constants';
import OHRIExtensionParcel from '../components/extension/ohri-extension-parcel.component';
import { EncounterDatetimeHandler } from '../submission-handlers/encounterDatetimeHandler';
import { UISelectExtended } from '../components/inputs/ui-select-extended/ui-select-extended';

import { BaseOpenMRSDataSource } from '../datasources/data-source';
import { LocationDataSource } from '../datasources/location-data-source';
export interface RegistryItem {
id: string;
component: any;
Expand Down Expand Up @@ -198,7 +199,12 @@ const fieldValidators: Array<ValidatorRegistryItem> = [
},
];

const dataSources: Array<DataSourceRegistryItem> = [];
const dataSources: Array<DataSourceRegistryItem> = [
{
id: 'concept_location',
component: new LocationDataSource(),
},
];

export const getFieldComponent = renderType => {
let lazy = baseFieldComponents.find(item => item.type == renderType || item?.alias == renderType)?.loadControl;
Expand Down

0 comments on commit fabd48d

Please sign in to comment.