Skip to content

Commit cb6a495

Browse files
committed
code review
1 parent 5510d0b commit cb6a495

13 files changed

+144
-93
lines changed

Diff for: src/components/inputs/ui-select-extended/ui-select-extended.component.tsx

+1-1
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 || items.find((item) => item.uuid == field.value)) {
173+
if (!inputValue) {
174174
// Carbon's initial call at component mount
175175
return true;
176176
}

Diff for: src/hooks/useFormJson.ts

+15-8
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,21 @@ async function refineFormJson(
123123
schemaTransformers: FormSchemaTransformer[] = [],
124124
formSessionIntent?: string,
125125
): Promise<FormSchema> {
126-
removeInlineSubForms(formJson, formSessionIntent);
127-
// apply form schema transformers
128-
for (let transformer of schemaTransformers) {
129-
const draftForm = await transformer.transform(formJson);
130-
formJson = draftForm;
131-
}
132-
setEncounterType(formJson);
133-
return applyFormIntent(formSessionIntent, formJson);
126+
await removeInlineSubForms(formJson, formSessionIntent);
127+
const transformedFormJson = await schemaTransformers.reduce(async (form, transformer) => {
128+
const currentForm = await form;
129+
if (isPromise(transformer.transform(currentForm))) {
130+
return transformer.transform(currentForm);
131+
} else {
132+
return transformer.transform(currentForm);
133+
}
134+
}, Promise.resolve(formJson));
135+
setEncounterType(transformedFormJson);
136+
return applyFormIntent(formSessionIntent, transformedFormJson);
137+
}
138+
139+
function isPromise(value: any): value is Promise<any> {
140+
return value && typeof value.then === 'function';
134141
}
135142

136143
/**

Diff for: src/hooks/usePersonAttributes.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import { openmrsFetch, type PersonAttribute, restBaseUrl } from '@openmrs/esm-framework';
22
import { useEffect, useState } from 'react';
3+
import { type FormSchema } from '../types';
34

4-
export const usePersonAttributes = (patientUuid: string) => {
5+
export const usePersonAttributes = (patientUuid: string, formJson: FormSchema) => {
56
const [personAttributes, setPersonAttributes] = useState<Array<PersonAttribute>>([]);
67
const [isLoading, setIsLoading] = useState(true);
78
const [error, setError] = useState(null);
89

910
useEffect(() => {
10-
if (patientUuid) {
11+
if (formJson.meta?.personAttributes?.hasPersonAttributeFields && patientUuid) {
1112
openmrsFetch(`${restBaseUrl}/patient/${patientUuid}?v=custom:(attributes)`)
1213
.then((response) => {
13-
setPersonAttributes(response?.data?.attributes);
14+
setPersonAttributes(response.data?.attributes);
1415
setIsLoading(false);
1516
})
1617
.catch((error) => {

Diff for: src/processors/encounter/encounter-form-processor.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ function useCustomHooks(context: Partial<FormProcessorContextProps>) {
4343
context.patient?.id,
4444
context.formJson,
4545
);
46-
const { isLoading: isLoadingPersonAttributes, personAttributes } = usePersonAttributes(context.patient?.id);
46+
const { isLoading: isLoadingPersonAttributes, personAttributes } = usePersonAttributes(
47+
context.patient?.id,
48+
context.formJson,
49+
);
4750

4851
useEffect(() => {
4952
setIsLoading(isLoadingPatientPrograms || isLoadingEncounter || isLoadingEncounterRole || isLoadingPersonAttributes);
@@ -170,9 +173,9 @@ export class EncounterFormProcessor extends FormProcessor {
170173

171174
// save person attributes
172175
try {
173-
const personattributes = preparePersonAttributes(context.formFields, context.location?.uuid);
174-
const savedPrograms = await savePersonAttributes(context.patient, personattributes);
175-
if (savedPrograms?.length) {
176+
const personAttributes = preparePersonAttributes(context.formFields, context.location?.uuid);
177+
const savedAttributes = await savePersonAttributes(context.patient, personAttributes);
178+
if (savedAttributes?.length) {
176179
showSnackbar({
177180
title: translateFn('personAttributesSaved', 'Person attribute(s) saved successfully'),
178181
kind: 'success',
@@ -181,12 +184,12 @@ export class EncounterFormProcessor extends FormProcessor {
181184
}
182185
} catch (error) {
183186
const errorMessages = extractErrorMessagesFromResponse(error);
184-
return Promise.reject({
187+
throw {
185188
title: translateFn('errorSavingPersonAttributes', 'Error saving person attributes'),
186189
description: errorMessages.join(', '),
187190
kind: 'error',
188191
critical: true,
189-
});
192+
};
190193
}
191194

192195
// save encounter

Diff for: src/processors/encounter/encounter-processor-helper.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,7 @@ export function saveAttachments(fields: FormField[], encounter: OpenmrsEncounter
154154
}
155155

156156
export function savePersonAttributes(patient: fhir.Patient, attributes: PersonAttribute[]) {
157-
return attributes.map((personAttribute) => {
158-
return savePersonAttribute(personAttribute, patient.id);
159-
});
157+
return attributes.map((personAttribute) => savePersonAttribute(personAttribute, patient.id));
160158
}
161159

162160
export function getMutableSessionProps(context: FormContextProps) {

Diff for: src/registry/inbuilt-components/control-templates.ts

-6
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,6 @@ export const controlTemplates: Array<ControlTemplate> = [
5050
},
5151
},
5252
},
53-
{
54-
name: 'person-attribute-location',
55-
datasource: {
56-
name: 'person_attribute_location_datasource',
57-
},
58-
},
5953
];
6054

6155
export const getControlTemplate = (name: string) => {

Diff for: src/registry/inbuilt-components/inbuiltDataSources.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { LocationDataSource } from '../../datasources/location-data-source';
55
import { ProviderDataSource } from '../../datasources/provider-datasource';
66
import { SelectConceptAnswersDatasource } from '../../datasources/select-concept-answers-datasource';
77
import { EncounterRoleDataSource } from '../../datasources/encounter-role-datasource';
8-
import { PersonAttributeLocationDataSource } from '../../datasources/person-attribute-datasource';
98

109
/**
1110
* @internal
@@ -36,8 +35,8 @@ export const inbuiltDataSources: Array<RegistryItem<DataSource<any>>> = [
3635
component: new EncounterRoleDataSource(),
3736
},
3837
{
39-
name: 'person_attribute_location_datasource',
40-
component: new PersonAttributeLocationDataSource(),
38+
name: 'person-attribute-location',
39+
component: new LocationDataSource(),
4140
},
4241
];
4342

Diff for: src/registry/inbuilt-components/inbuiltTransformers.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { PersonAttributesTransformer } from '../../transformers/person-attributes-transformer';
12
import { DefaultFormSchemaTransformer } from '../../transformers/default-schema-transformer';
23
import { type FormSchemaTransformer } from '../../types';
34
import { type RegistryItem } from '../registry';
@@ -7,4 +8,8 @@ export const inbuiltFormTransformers: Array<RegistryItem<FormSchemaTransformer>>
78
name: 'DefaultFormSchemaTransformer',
89
component: DefaultFormSchemaTransformer,
910
},
11+
{
12+
name: 'PersonAttributesTransformer',
13+
component: PersonAttributesTransformer,
14+
},
1015
];

Diff for: src/registry/inbuilt-components/template-component-map.ts

-4
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,4 @@ export const templateToComponentMap = [
2525
name: 'encounter-role',
2626
baseControlComponent: UiSelectExtended,
2727
},
28-
{
29-
name: 'person_attribute_location_datasource',
30-
baseControlComponent: UiSelectExtended,
31-
},
3228
];

Diff for: src/registry/registry.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,10 @@ export async function getRegisteredFieldValueAdapter(type: string): Promise<Form
171171
}
172172

173173
export async function getRegisteredFormSchemaTransformers(): Promise<FormSchemaTransformer[]> {
174-
const transformers: FormSchemaTransformer[] = [];
174+
const transformers: Array<FormSchemaTransformer> = [];
175175

176176
const cachedTransformers = registryCache.formSchemaTransformers;
177+
177178
if (Object.keys(cachedTransformers).length) {
178179
return Object.values(cachedTransformers);
179180
}

Diff for: src/transformers/default-schema-transformer.ts

+20-58
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,34 @@ import { type OpenmrsResource } from '@openmrs/esm-framework';
22
import { type FormField, type FormSchema, type FormSchemaTransformer, type RenderType, type FormPage } from '../types';
33
import { isTrue } from '../utils/boolean-utils';
44
import { hasRendering } from '../utils/common-utils';
5-
import { getPersonAttributeTypeFormat } from '../api/';
65

76
export type RenderTypeExtended = 'multiCheckbox' | 'numeric' | RenderType;
87

98
export const DefaultFormSchemaTransformer: FormSchemaTransformer = {
10-
transform: async (form: FormSchema): Promise<FormSchema> => {
11-
try {
12-
parseBooleanTokenIfPresent(form, 'readonly');
13-
for (const [index, page] of form.pages.entries()) {
14-
const label = page.label ?? '';
15-
page.id = `page-${label.replace(/\s/g, '')}-${index}`;
16-
parseBooleanTokenIfPresent(page, 'readonly');
17-
if (page.sections) {
18-
for (const section of page.sections) {
19-
section.questions = handleQuestionsWithDateOptions(section.questions);
20-
section.questions = handleQuestionsWithObsComments(section.questions);
21-
parseBooleanTokenIfPresent(section, 'readonly');
22-
parseBooleanTokenIfPresent(section, 'isExpanded');
23-
if (section.questions) {
24-
section.questions = await Promise.all(
25-
section.questions.map((question) => handleQuestion(question, page, form)),
26-
);
27-
}
28-
}
29-
}
9+
transform: (form: FormSchema) => {
10+
parseBooleanTokenIfPresent(form, 'readonly');
11+
form.pages.forEach((page, index) => {
12+
const label = page.label ?? '';
13+
page.id = `page-${label.replace(/\s/g, '')}-${index}`;
14+
parseBooleanTokenIfPresent(page, 'readonly');
15+
if (page.sections) {
16+
page.sections.forEach((section) => {
17+
section.questions = handleQuestionsWithDateOptions(section.questions);
18+
section.questions = handleQuestionsWithObsComments(section.questions);
19+
parseBooleanTokenIfPresent(section, 'readonly');
20+
parseBooleanTokenIfPresent(section, 'isExpanded');
21+
section?.questions?.forEach((question, index) => handleQuestion(question, page, form));
22+
});
3023
}
31-
} catch (error) {
32-
console.error('Error in form transformation:', error);
33-
throw error;
34-
}
24+
});
3525
if (form.meta?.programs) {
3626
handleProgramMetaTags(form);
3727
}
3828
return form;
3929
},
4030
};
4131

42-
async function handleQuestion(question: FormField, page: FormPage, form: FormSchema): Promise<FormField> {
32+
function handleQuestion(question: FormField, page: FormPage, form: FormSchema) {
4333
if (question.type === 'programState') {
4434
const formMeta = form.meta ?? {};
4535
formMeta.programs = formMeta.programs
@@ -50,20 +40,17 @@ async function handleQuestion(question: FormField, page: FormPage, form: FormSch
5040
try {
5141
sanitizeQuestion(question);
5242
setFieldValidators(question);
53-
await transformByType(question);
43+
transformByType(question);
5444
transformByRendering(question);
5545

5646
if (question.questions?.length) {
5747
if (question.type === 'obsGroup' && question.questions.length) {
5848
question.questions.forEach((nestedQuestion) => handleQuestion(nestedQuestion, page, form));
5949
} else {
60-
question.questions = await Promise.all(
61-
question.questions.map((nestedQuestion) => handleQuestion(nestedQuestion, page, form)),
62-
);
50+
question.questions.forEach((nestedQuestion) => handleQuestion(nestedQuestion, page, form));
6351
}
6452
}
6553
question.meta.pageId = page.id;
66-
return question;
6754
} catch (error) {
6855
console.error(error);
6956
}
@@ -121,7 +108,7 @@ function sanitizeQuestion(question: FormField) {
121108
}
122109
}
123110

124-
function parseBooleanTokenIfPresent(node: any, token: any) {
111+
export function parseBooleanTokenIfPresent(node: any, token: any) {
125112
if (node && typeof node[token] === 'string') {
126113
const trimmed = node[token].trim().toLowerCase();
127114
if (trimmed === 'true' || trimmed === 'false') {
@@ -145,7 +132,7 @@ function setFieldValidators(question: FormField) {
145132
}
146133
}
147134

148-
async function transformByType(question: FormField) {
135+
function transformByType(question: FormField) {
149136
switch (question.type) {
150137
case 'encounterProvider':
151138
question.questionOptions.rendering = 'encounter-provider';
@@ -161,9 +148,6 @@ async function transformByType(question: FormField) {
161148
? 'date'
162149
: question.questionOptions.rendering;
163150
break;
164-
case 'personAttribute':
165-
await handlePersonAttributeType(question);
166-
break;
167151
}
168152
}
169153

@@ -292,25 +276,3 @@ function handleQuestionsWithObsComments(sectionQuestions: Array<FormField>): Arr
292276

293277
return augmentedQuestions;
294278
}
295-
296-
async function handlePersonAttributeType(question: FormField) {
297-
if (question.questionOptions.rendering !== 'text') {
298-
question.questionOptions.rendering === 'ui-select-extended';
299-
}
300-
301-
const attributeTypeFormat = await getPersonAttributeTypeFormat(question.questionOptions.attributeType);
302-
if (attributeTypeFormat === 'org.openmrs.Location') {
303-
question.questionOptions.datasource = {
304-
name: 'person_attribute_location_datasource',
305-
};
306-
} else if (attributeTypeFormat === 'org.openmrs.Concept') {
307-
question.questionOptions.datasource = {
308-
name: 'select_concept_answers_datasource',
309-
config: {
310-
concept: question.questionOptions?.concept,
311-
},
312-
};
313-
} else if (attributeTypeFormat === 'java.lang.String') {
314-
question.questionOptions.rendering = 'text';
315-
}
316-
}

Diff for: src/transformers/person-attributes-transformer.ts

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { type FormField, type FormSchema, type FormSchemaTransformer, type RenderType, type FormPage } from '../types';
2+
import { getPersonAttributeTypeFormat } from '../api/';
3+
import { parseBooleanTokenIfPresent } from './default-schema-transformer';
4+
5+
export type RenderTypeExtended = 'multiCheckbox' | 'numeric' | RenderType;
6+
7+
export const PersonAttributesTransformer: FormSchemaTransformer = {
8+
transform: async (form: FormSchema): Promise<FormSchema> => {
9+
try {
10+
parseBooleanTokenIfPresent(form, 'readonly');
11+
for (const [index, page] of form.pages.entries()) {
12+
const label = page.label ?? '';
13+
page.id = `page-${label.replace(/\s/g, '')}-${index}`;
14+
parseBooleanTokenIfPresent(page, 'readonly');
15+
if (page.sections) {
16+
for (const section of page.sections) {
17+
if (section.questions) {
18+
const formMeta = form.meta ?? {};
19+
if (checkQuestions(section.questions)) {
20+
formMeta.personAttributes = { hasPersonAttributeFields: true };
21+
}
22+
form.meta = formMeta;
23+
section.questions = await Promise.all(
24+
section.questions.map((question) => handleQuestion(question, page, form)),
25+
);
26+
}
27+
}
28+
}
29+
}
30+
} catch (error) {
31+
console.error('Error in form transformation:', error);
32+
throw error;
33+
}
34+
return form;
35+
},
36+
};
37+
38+
async function handleQuestion(question: FormField, page: FormPage, form: FormSchema): Promise<FormField> {
39+
try {
40+
await transformByType(question);
41+
if (question.questions?.length) {
42+
question.questions = await Promise.all(
43+
question.questions.map((nestedQuestion) => handleQuestion(nestedQuestion, page, form)),
44+
);
45+
}
46+
question.meta.pageId = page.id;
47+
return question;
48+
} catch (error) {
49+
console.error(error);
50+
}
51+
}
52+
53+
async function transformByType(question: FormField) {
54+
switch (question.type) {
55+
case 'personAttribute':
56+
await handlePersonAttributeType(question);
57+
break;
58+
}
59+
}
60+
61+
async function handlePersonAttributeType(question: FormField) {
62+
const attributeTypeFormat = await getPersonAttributeTypeFormat(question.questionOptions.attributeType);
63+
if (attributeTypeFormat === 'org.openmrs.Location') {
64+
question.questionOptions.datasource = {
65+
name: 'location_datasource',
66+
};
67+
} else if (attributeTypeFormat === 'org.openmrs.Concept') {
68+
question.questionOptions.datasource = {
69+
name: 'select_concept_answers_datasource',
70+
config: {
71+
concept: question.questionOptions?.concept,
72+
},
73+
};
74+
} else if (attributeTypeFormat === 'java.lang.String') {
75+
question.questionOptions.rendering = 'text';
76+
}
77+
}
78+
79+
function checkQuestions(questions) {
80+
return questions.some((question) => question.type === 'personAttribute');
81+
}

0 commit comments

Comments
 (0)