Skip to content

Commit e0f7bcc

Browse files
committed
(feat) O3-3367 Add support for person attributes
1 parent 5401a64 commit e0f7bcc

22 files changed

+312
-43
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { type PersonAttribute, type OpenmrsResource } from '@openmrs/esm-framework';
2+
import { type FormContextProps } from '../provider/form-provider';
3+
import { type FormField, type FormFieldValueAdapter, type FormProcessorContextProps } from '../types';
4+
import { clearSubmission } from '../utils/common-utils';
5+
import { isEmpty } from '../validators/form-validator';
6+
7+
export const PersonAttributesAdapter: FormFieldValueAdapter = {
8+
transformFieldValue: function (field: FormField, value: any, context: FormContextProps) {
9+
clearSubmission(field);
10+
if (field.meta?.previousValue?.value === value || isEmpty(value)) {
11+
return null;
12+
}
13+
field.meta.submission.newValue = {
14+
value: value,
15+
attributeType: field.questionOptions?.attribute?.type,
16+
};
17+
return field.meta.submission.newValue;
18+
},
19+
getInitialValue: function (field: FormField, sourceObject: OpenmrsResource, context: FormProcessorContextProps) {
20+
const rendering = field.questionOptions.rendering;
21+
22+
const personAttributeValue = context?.customDependencies.personAttributes.find(
23+
(attribute: PersonAttribute) => attribute.attributeType.uuid === field.questionOptions.attribute?.type,
24+
)?.value;
25+
if (rendering === 'text') {
26+
if (typeof personAttributeValue === 'string') {
27+
return personAttributeValue;
28+
} else if (
29+
personAttributeValue &&
30+
typeof personAttributeValue === 'object' &&
31+
'display' in personAttributeValue
32+
) {
33+
return personAttributeValue?.display;
34+
}
35+
} else if (rendering === 'ui-select-extended') {
36+
if (personAttributeValue && typeof personAttributeValue === 'object' && 'uuid' in personAttributeValue) {
37+
return personAttributeValue?.uuid;
38+
}
39+
}
40+
return null;
41+
},
42+
getPreviousValue: function (field: FormField, sourceObject: OpenmrsResource, context: FormProcessorContextProps) {
43+
return null;
44+
},
45+
getDisplayValue: function (field: FormField, value: any) {
46+
if (value?.display) {
47+
return value.display;
48+
}
49+
return value;
50+
},
51+
tearDown: function (): void {
52+
return;
53+
},
54+
};

src/api/index.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fhirBaseUrl, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
1+
import { fhirBaseUrl, openmrsFetch, type PersonAttribute, restBaseUrl } from '@openmrs/esm-framework';
22
import { encounterRepresentation } from '../constants';
33
import { type OpenmrsForm, type PatientIdentifier, type PatientProgramPayload } from '../types';
44
import { isUuid } from '../utils/boolean-utils';
@@ -180,3 +180,36 @@ export function savePatientIdentifier(patientIdentifier: PatientIdentifier, pati
180180
body: JSON.stringify(patientIdentifier),
181181
});
182182
}
183+
184+
export function savePersonAttribute(personAttribute: PersonAttribute, personUuid: string) {
185+
let url: string;
186+
187+
if (personAttribute.uuid) {
188+
url = `${restBaseUrl}/person/${personUuid}/attribute/${personAttribute.uuid}`;
189+
} else {
190+
url = `${restBaseUrl}/person/${personUuid}/attribute`;
191+
}
192+
193+
return openmrsFetch(url, {
194+
headers: {
195+
'Content-Type': 'application/json',
196+
},
197+
method: 'POST',
198+
body: JSON.stringify(personAttribute),
199+
});
200+
}
201+
202+
export async function getPersonAttributeTypeFormat(personAttributeTypeUuid: string) {
203+
try {
204+
const response = await openmrsFetch(
205+
`${restBaseUrl}/personattributetype/${personAttributeTypeUuid}?v=custom:(format)`,
206+
);
207+
if (response) {
208+
const { data } = response;
209+
return data?.format;
210+
}
211+
return null;
212+
} catch (error) {
213+
return null;
214+
}
215+
}

src/components/inputs/ui-select-extended/ui-select-extended.component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ const UiSelectExtended: React.FC<FormFieldInputProps> = ({ field, errors, warnin
149149
selectedItem={selectedItem}
150150
placeholder={isSearchable ? t('search', 'Search') + '...' : null}
151151
shouldFilterItem={({ item, inputValue }) => {
152-
if (!inputValue) {
152+
if (!inputValue || items.find((item) => item.uuid == field.value)) {
153153
// Carbon's initial call at component mount
154154
return true;
155155
}

src/components/inputs/ui-select-extended/ui-select-extended.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,14 @@ jest.mock('../../../registry/registry', () => {
107107
};
108108
});
109109

110+
jest.mock('../../../hooks/usePersonAttributes', () => ({
111+
usePersonAttributes: jest.fn().mockReturnValue({
112+
personAttributes: [],
113+
error: null,
114+
isLoading: false,
115+
}),
116+
}));
117+
110118
const encounter = {
111119
uuid: 'encounter-uuid',
112120
obs: [

src/components/inputs/unspecified/unspecified.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ jest.mock('../../../hooks/useEncounter', () => ({
5858
}),
5959
}));
6060

61+
jest.mock('../../../hooks/usePersonAttributes', () => ({
62+
usePersonAttributes: jest.fn().mockReturnValue({
63+
personAttributes: [],
64+
error: null,
65+
isLoading: false,
66+
}),
67+
}));
68+
6169
const renderForm = async (mode: SessionMode = 'enter') => {
6270
await act(async () => {
6371
render(
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2+
import { BaseOpenMRSDataSource } from './data-source';
3+
4+
export class PersonAttributeLocationDataSource extends BaseOpenMRSDataSource {
5+
constructor() {
6+
super(null);
7+
}
8+
9+
async fetchData(searchTerm: string, config?: Record<string, any>, uuid?: string): Promise<any[]> {
10+
const rep = 'v=custom:(uuid,display)';
11+
const url = `${restBaseUrl}/location?${rep}`;
12+
const { data } = await openmrsFetch(searchTerm ? `${url}&q=${searchTerm}` : url);
13+
14+
return data?.results;
15+
}
16+
}

src/datasources/select-concept-answers-datasource.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export class SelectConceptAnswersDatasource extends BaseOpenMRSDataSource {
77
}
88

99
fetchData(searchTerm: string, config?: Record<string, any>): Promise<any[]> {
10-
const apiUrl = this.url.replace('conceptUuid', config.referencedValue || config.concept);
10+
const apiUrl = this.url.replace('conceptUuid', config.concept || config.referencedValue);
1111
return openmrsFetch(apiUrl).then(({ data }) => {
1212
return data['setMembers'].length ? data['setMembers'] : data['answers'];
1313
});

src/form-engine.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@ jest.mock('./hooks/useConcepts', () => ({
118118
}),
119119
}));
120120

121+
jest.mock('./hooks/usePersonAttributes', () => ({
122+
usePersonAttributes: jest.fn().mockReturnValue({
123+
personAttributes: [],
124+
error: null,
125+
isLoading: false,
126+
}),
127+
}));
128+
121129
describe('Form engine component', () => {
122130
const user = userEvent.setup();
123131

src/hooks/useFormJson.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,17 @@ function validateFormsArgs(formUuid: string, rawFormJson: any): Error {
108108
* @param {string} [formSessionIntent] - The optional form session intent.
109109
* @returns {FormSchema} - The refined form JSON object of type FormSchema.
110110
*/
111-
function refineFormJson(
111+
async function refineFormJson(
112112
formJson: any,
113113
schemaTransformers: FormSchemaTransformer[] = [],
114114
formSessionIntent?: string,
115-
): FormSchema {
115+
): Promise<FormSchema> {
116116
removeInlineSubForms(formJson, formSessionIntent);
117117
// apply form schema transformers
118-
schemaTransformers.reduce((draftForm, transformer) => transformer.transform(draftForm), formJson);
118+
for (let transformer of schemaTransformers) {
119+
const draftForm = await transformer.transform(formJson);
120+
formJson = draftForm;
121+
}
119122
setEncounterType(formJson);
120123
return applyFormIntent(formSessionIntent, formJson);
121124
}
@@ -134,7 +137,7 @@ function parseFormJson(formJson: any): FormSchema {
134137
* @param {FormSchema} formJson - The input form JSON object of type FormSchema.
135138
* @param {string} formSessionIntent - The form session intent.
136139
*/
137-
function removeInlineSubForms(formJson: FormSchema, formSessionIntent: string): void {
140+
async function removeInlineSubForms(formJson: FormSchema, formSessionIntent: string): Promise<void> {
138141
for (let i = formJson.pages.length - 1; i >= 0; i--) {
139142
const page = formJson.pages[i];
140143
if (
@@ -143,7 +146,7 @@ function removeInlineSubForms(formJson: FormSchema, formSessionIntent: string):
143146
page.subform?.form?.encounterType === formJson.encounterType
144147
) {
145148
const nonSubformPages = page.subform.form.pages.filter((page) => !isTrue(page.isSubform));
146-
formJson.pages.splice(i, 1, ...refineFormJson(page.subform.form, [], formSessionIntent).pages);
149+
formJson.pages.splice(i, 1, ...(await refineFormJson(page.subform.form, [], formSessionIntent)).pages);
147150
}
148151
}
149152
}

src/hooks/usePersonAttributes.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { openmrsFetch, type PersonAttribute, restBaseUrl } from '@openmrs/esm-framework';
2+
import { useEffect, useState } from 'react';
3+
4+
export const usePersonAttributes = (patientUuid: string) => {
5+
const [personAttributes, setPersonAttributes] = useState<Array<PersonAttribute>>([]);
6+
const [isLoading, setIsLoading] = useState(true);
7+
const [error, setError] = useState(null);
8+
9+
useEffect(() => {
10+
if (patientUuid) {
11+
openmrsFetch(`${restBaseUrl}/patient/${patientUuid}?v=custom:(attributes)`)
12+
.then((response) => {
13+
setPersonAttributes(response?.data?.attributes);
14+
setIsLoading(false);
15+
})
16+
.catch((error) => {
17+
setError(error);
18+
setIsLoading(false);
19+
});
20+
} else {
21+
setIsLoading(false);
22+
}
23+
}, [patientUuid]);
24+
25+
return {
26+
personAttributes,
27+
error,
28+
isLoading: isLoading,
29+
};
30+
};

0 commit comments

Comments
 (0)