Skip to content

Commit 5510d0b

Browse files
committed
(feat) O3-3367 Add support for person attributes
1 parent 39c7051 commit 5510d0b

22 files changed

+309
-44
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?.attributeType,
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.attributeType,
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 { FHIRObsResource, OpenmrsForm, PatientIdentifier, PatientProgramPayload } from '../types';
44
import { isUuid } from '../utils/boolean-utils';
@@ -183,3 +183,36 @@ export function savePatientIdentifier(patientIdentifier: PatientIdentifier, pati
183183
body: JSON.stringify(patientIdentifier),
184184
});
185185
}
186+
187+
export function savePersonAttribute(personAttribute: PersonAttribute, personUuid: string) {
188+
let url: string;
189+
190+
if (personAttribute.uuid) {
191+
url = `${restBaseUrl}/person/${personUuid}/attribute/${personAttribute.uuid}`;
192+
} else {
193+
url = `${restBaseUrl}/person/${personUuid}/attribute`;
194+
}
195+
196+
return openmrsFetch(url, {
197+
headers: {
198+
'Content-Type': 'application/json',
199+
},
200+
method: 'POST',
201+
body: JSON.stringify(personAttribute),
202+
});
203+
}
204+
205+
export async function getPersonAttributeTypeFormat(personAttributeTypeUuid: string) {
206+
try {
207+
const response = await openmrsFetch(
208+
`${restBaseUrl}/personattributetype/${personAttributeTypeUuid}?v=custom:(format)`,
209+
);
210+
if (response) {
211+
const { data } = response;
212+
return data?.format;
213+
}
214+
return null;
215+
} catch (error) {
216+
return null;
217+
}
218+
}

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
@@ -170,7 +170,7 @@ const UiSelectExtended: React.FC<FormFieldInputProps> = ({ field, errors, warnin
170170
selectedItem={selectedItem}
171171
placeholder={isSearchable ? t('search', 'Search') + '...' : null}
172172
shouldFilterItem={({ item, inputValue }) => {
173-
if (!inputValue) {
173+
if (!inputValue || items.find((item) => item.uuid == field.value)) {
174174
// Carbon's initial call at component mount
175175
return true;
176176
}

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
@@ -106,6 +106,14 @@ jest.mock('../../../registry/registry', () => {
106106
};
107107
});
108108

109+
jest.mock('../../../hooks/usePersonAttributes', () => ({
110+
usePersonAttributes: jest.fn().mockReturnValue({
111+
personAttributes: [],
112+
error: null,
113+
isLoading: false,
114+
}),
115+
}));
116+
109117
const encounter = {
110118
uuid: 'encounter-uuid',
111119
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
@@ -117,6 +117,14 @@ jest.mock('./hooks/useConcepts', () => ({
117117
}),
118118
}));
119119

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

src/hooks/useFormJson.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,17 @@ function validateFormsArgs(formUuid: string, rawFormJson: any): Error {
118118
* @param {string} [formSessionIntent] - The optional form session intent.
119119
* @returns {FormSchema} - The refined form JSON object of type FormSchema.
120120
*/
121-
function refineFormJson(
121+
async function refineFormJson(
122122
formJson: any,
123123
schemaTransformers: FormSchemaTransformer[] = [],
124124
formSessionIntent?: string,
125-
): FormSchema {
125+
): Promise<FormSchema> {
126126
removeInlineSubForms(formJson, formSessionIntent);
127127
// apply form schema transformers
128-
schemaTransformers.reduce((draftForm, transformer) => transformer.transform(draftForm), formJson);
128+
for (let transformer of schemaTransformers) {
129+
const draftForm = await transformer.transform(formJson);
130+
formJson = draftForm;
131+
}
129132
setEncounterType(formJson);
130133
return applyFormIntent(formSessionIntent, formJson);
131134
}
@@ -144,7 +147,7 @@ function parseFormJson(formJson: any): FormSchema {
144147
* @param {FormSchema} formJson - The input form JSON object of type FormSchema.
145148
* @param {string} formSessionIntent - The form session intent.
146149
*/
147-
function removeInlineSubForms(formJson: FormSchema, formSessionIntent: string): void {
150+
async function removeInlineSubForms(formJson: FormSchema, formSessionIntent: string): Promise<void> {
148151
for (let i = formJson.pages.length - 1; i >= 0; i--) {
149152
const page = formJson.pages[i];
150153
if (
@@ -153,7 +156,7 @@ function removeInlineSubForms(formJson: FormSchema, formSessionIntent: string):
153156
page.subform?.form?.encounterType === formJson.encounterType
154157
) {
155158
const nonSubformPages = page.subform.form.pages.filter((page) => !isTrue(page.isSubform));
156-
formJson.pages.splice(i, 1, ...refineFormJson(page.subform.form, [], formSessionIntent).pages);
159+
formJson.pages.splice(i, 1, ...(await refineFormJson(page.subform.form, [], formSessionIntent)).pages);
157160
}
158161
}
159162
}

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)