Skip to content

Commit e967865

Browse files
committed
code review
1 parent 5510d0b commit e967865

13 files changed

+132
-89
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

+4-4
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,10 @@ async function refineFormJson(
125125
): Promise<FormSchema> {
126126
removeInlineSubForms(formJson, formSessionIntent);
127127
// apply form schema transformers
128-
for (let transformer of schemaTransformers) {
129-
const draftForm = await transformer.transform(formJson);
130-
formJson = draftForm;
131-
}
128+
schemaTransformers.reduce(
129+
async (form, transformer) => Promise.resolve(transformer.transform(await form)),
130+
Promise.resolve(formJson),
131+
);
132132
setEncounterType(formJson);
133133
return applyFormIntent(formSessionIntent, formJson);
134134
}

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

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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+
if (checkQuestions(section.questions)) {
19+
const formMeta = form.meta ?? {};
20+
formMeta.personAttributes = { hasPersonAttributeFields: true };
21+
}
22+
section.questions = await Promise.all(
23+
section.questions.map((question) => handleQuestion(question, page, form)),
24+
);
25+
}
26+
}
27+
}
28+
}
29+
} catch (error) {
30+
console.error('Error in form transformation:', error);
31+
throw error;
32+
}
33+
return form;
34+
},
35+
};
36+
37+
async function handleQuestion(question: FormField, page: FormPage, form: FormSchema): Promise<FormField> {
38+
try {
39+
await transformByType(question);
40+
if (question.questions?.length) {
41+
question.questions = await Promise.all(
42+
question.questions.map((nestedQuestion) => handleQuestion(nestedQuestion, page, form)),
43+
);
44+
}
45+
question.meta.pageId = page.id;
46+
return question;
47+
} catch (error) {
48+
console.error(error);
49+
}
50+
}
51+
52+
async function transformByType(question: FormField) {
53+
switch (question.type) {
54+
case 'personAttribute':
55+
await handlePersonAttributeType(question);
56+
break;
57+
}
58+
}
59+
60+
async function handlePersonAttributeType(question: FormField) {
61+
const attributeTypeFormat = await getPersonAttributeTypeFormat(question.questionOptions.attributeType);
62+
if (attributeTypeFormat === 'org.openmrs.Location') {
63+
question.questionOptions.datasource = {
64+
name: 'location_datasource',
65+
};
66+
} else if (attributeTypeFormat === 'org.openmrs.Concept') {
67+
question.questionOptions.datasource = {
68+
name: 'select_concept_answers_datasource',
69+
config: {
70+
concept: question.questionOptions?.concept,
71+
},
72+
};
73+
} else if (attributeTypeFormat === 'java.lang.String') {
74+
question.questionOptions.rendering = 'text';
75+
}
76+
}
77+
78+
function checkQuestions(questions) {
79+
return questions.some((question) => question.type === 'personAttribute');
80+
}

Diff for: src/types/schema.ts

+4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export interface FormSchema {
2525
hasProgramFields?: boolean;
2626
[anythingElse: string]: any;
2727
};
28+
personAttributes?: {
29+
hasPersonAttributeFields?: boolean;
30+
[anythingElse: string]: any;
31+
};
2832
};
2933
}
3034

0 commit comments

Comments
 (0)