Skip to content

Commit

Permalink
code review
Browse files Browse the repository at this point in the history
  • Loading branch information
CynthiaKamau committed Dec 13, 2024
1 parent 5510d0b commit e967865
Show file tree
Hide file tree
Showing 13 changed files with 132 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ const UiSelectExtended: React.FC<FormFieldInputProps> = ({ field, errors, warnin
selectedItem={selectedItem}
placeholder={isSearchable ? t('search', 'Search') + '...' : null}
shouldFilterItem={({ item, inputValue }) => {
if (!inputValue || items.find((item) => item.uuid == field.value)) {
if (!inputValue) {
// Carbon's initial call at component mount
return true;
}
Expand Down
8 changes: 4 additions & 4 deletions src/hooks/useFormJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,10 @@ async function refineFormJson(
): Promise<FormSchema> {
removeInlineSubForms(formJson, formSessionIntent);
// apply form schema transformers
for (let transformer of schemaTransformers) {
const draftForm = await transformer.transform(formJson);
formJson = draftForm;
}
schemaTransformers.reduce(
async (form, transformer) => Promise.resolve(transformer.transform(await form)),
Promise.resolve(formJson),
);
setEncounterType(formJson);
return applyFormIntent(formSessionIntent, formJson);
}
Expand Down
7 changes: 4 additions & 3 deletions src/hooks/usePersonAttributes.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { openmrsFetch, type PersonAttribute, restBaseUrl } from '@openmrs/esm-framework';
import { useEffect, useState } from 'react';
import { type FormSchema } from '../types';

export const usePersonAttributes = (patientUuid: string) => {
export const usePersonAttributes = (patientUuid: string, formJson: FormSchema) => {
const [personAttributes, setPersonAttributes] = useState<Array<PersonAttribute>>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
if (patientUuid) {
if (formJson.meta?.personAttributes.hasPersonAttributeFields && patientUuid) {
openmrsFetch(`${restBaseUrl}/patient/${patientUuid}?v=custom:(attributes)`)
.then((response) => {
setPersonAttributes(response?.data?.attributes);
setPersonAttributes(response.data?.attributes);
setIsLoading(false);
})
.catch((error) => {
Expand Down
15 changes: 9 additions & 6 deletions src/processors/encounter/encounter-form-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ function useCustomHooks(context: Partial<FormProcessorContextProps>) {
context.patient?.id,
context.formJson,
);
const { isLoading: isLoadingPersonAttributes, personAttributes } = usePersonAttributes(context.patient?.id);
const { isLoading: isLoadingPersonAttributes, personAttributes } = usePersonAttributes(
context.patient?.id,
context.formJson,
);

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

// save person attributes
try {
const personattributes = preparePersonAttributes(context.formFields, context.location?.uuid);
const savedPrograms = await savePersonAttributes(context.patient, personattributes);
if (savedPrograms?.length) {
const personAttributes = preparePersonAttributes(context.formFields, context.location?.uuid);
const savedAttributes = await savePersonAttributes(context.patient, personAttributes);
if (savedAttributes?.length) {
showSnackbar({
title: translateFn('personAttributesSaved', 'Person attribute(s) saved successfully'),
kind: 'success',
Expand All @@ -181,12 +184,12 @@ export class EncounterFormProcessor extends FormProcessor {
}
} catch (error) {
const errorMessages = extractErrorMessagesFromResponse(error);
return Promise.reject({
throw {
title: translateFn('errorSavingPersonAttributes', 'Error saving person attributes'),
description: errorMessages.join(', '),
kind: 'error',
critical: true,
});
};
}

// save encounter
Expand Down
4 changes: 1 addition & 3 deletions src/processors/encounter/encounter-processor-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,7 @@ export function saveAttachments(fields: FormField[], encounter: OpenmrsEncounter
}

export function savePersonAttributes(patient: fhir.Patient, attributes: PersonAttribute[]) {
return attributes.map((personAttribute) => {
return savePersonAttribute(personAttribute, patient.id);
});
return attributes.map((personAttribute) => savePersonAttribute(personAttribute, patient.id));
}

export function getMutableSessionProps(context: FormContextProps) {
Expand Down
6 changes: 0 additions & 6 deletions src/registry/inbuilt-components/control-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,6 @@ export const controlTemplates: Array<ControlTemplate> = [
},
},
},
{
name: 'person-attribute-location',
datasource: {
name: 'person_attribute_location_datasource',
},
},
];

export const getControlTemplate = (name: string) => {
Expand Down
5 changes: 2 additions & 3 deletions src/registry/inbuilt-components/inbuiltDataSources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { LocationDataSource } from '../../datasources/location-data-source';
import { ProviderDataSource } from '../../datasources/provider-datasource';
import { SelectConceptAnswersDatasource } from '../../datasources/select-concept-answers-datasource';
import { EncounterRoleDataSource } from '../../datasources/encounter-role-datasource';
import { PersonAttributeLocationDataSource } from '../../datasources/person-attribute-datasource';

/**
* @internal
Expand Down Expand Up @@ -36,8 +35,8 @@ export const inbuiltDataSources: Array<RegistryItem<DataSource<any>>> = [
component: new EncounterRoleDataSource(),
},
{
name: 'person_attribute_location_datasource',
component: new PersonAttributeLocationDataSource(),
name: 'person-attribute-location',
component: new LocationDataSource(),
},
];

Expand Down
5 changes: 5 additions & 0 deletions src/registry/inbuilt-components/inbuiltTransformers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PersonAttributesTransformer } from '../../transformers/person-attributes-transformer';
import { DefaultFormSchemaTransformer } from '../../transformers/default-schema-transformer';
import { type FormSchemaTransformer } from '../../types';
import { type RegistryItem } from '../registry';
Expand All @@ -7,4 +8,8 @@ export const inbuiltFormTransformers: Array<RegistryItem<FormSchemaTransformer>>
name: 'DefaultFormSchemaTransformer',
component: DefaultFormSchemaTransformer,
},
{
name: 'PersonAttributesTransformer',
component: PersonAttributesTransformer,
},
];
4 changes: 0 additions & 4 deletions src/registry/inbuilt-components/template-component-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,4 @@ export const templateToComponentMap = [
name: 'encounter-role',
baseControlComponent: UiSelectExtended,
},
{
name: 'person_attribute_location_datasource',
baseControlComponent: UiSelectExtended,
},
];
3 changes: 2 additions & 1 deletion src/registry/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,10 @@ export async function getRegisteredFieldValueAdapter(type: string): Promise<Form
}

export async function getRegisteredFormSchemaTransformers(): Promise<FormSchemaTransformer[]> {
const transformers: FormSchemaTransformer[] = [];
const transformers: Array<FormSchemaTransformer> = [];

const cachedTransformers = registryCache.formSchemaTransformers;

if (Object.keys(cachedTransformers).length) {
return Object.values(cachedTransformers);
}
Expand Down
78 changes: 20 additions & 58 deletions src/transformers/default-schema-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,34 @@ import { type OpenmrsResource } from '@openmrs/esm-framework';
import { type FormField, type FormSchema, type FormSchemaTransformer, type RenderType, type FormPage } from '../types';
import { isTrue } from '../utils/boolean-utils';
import { hasRendering } from '../utils/common-utils';
import { getPersonAttributeTypeFormat } from '../api/';

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

export const DefaultFormSchemaTransformer: FormSchemaTransformer = {
transform: async (form: FormSchema): Promise<FormSchema> => {
try {
parseBooleanTokenIfPresent(form, 'readonly');
for (const [index, page] of form.pages.entries()) {
const label = page.label ?? '';
page.id = `page-${label.replace(/\s/g, '')}-${index}`;
parseBooleanTokenIfPresent(page, 'readonly');
if (page.sections) {
for (const section of page.sections) {
section.questions = handleQuestionsWithDateOptions(section.questions);
section.questions = handleQuestionsWithObsComments(section.questions);
parseBooleanTokenIfPresent(section, 'readonly');
parseBooleanTokenIfPresent(section, 'isExpanded');
if (section.questions) {
section.questions = await Promise.all(
section.questions.map((question) => handleQuestion(question, page, form)),
);
}
}
}
transform: (form: FormSchema) => {
parseBooleanTokenIfPresent(form, 'readonly');
form.pages.forEach((page, index) => {
const label = page.label ?? '';
page.id = `page-${label.replace(/\s/g, '')}-${index}`;
parseBooleanTokenIfPresent(page, 'readonly');
if (page.sections) {
page.sections.forEach((section) => {
section.questions = handleQuestionsWithDateOptions(section.questions);
section.questions = handleQuestionsWithObsComments(section.questions);
parseBooleanTokenIfPresent(section, 'readonly');
parseBooleanTokenIfPresent(section, 'isExpanded');
section?.questions?.forEach((question, index) => handleQuestion(question, page, form));
});
}
} catch (error) {
console.error('Error in form transformation:', error);
throw error;
}
});
if (form.meta?.programs) {
handleProgramMetaTags(form);
}
return form;
},
};

async function handleQuestion(question: FormField, page: FormPage, form: FormSchema): Promise<FormField> {
function handleQuestion(question: FormField, page: FormPage, form: FormSchema) {
if (question.type === 'programState') {
const formMeta = form.meta ?? {};
formMeta.programs = formMeta.programs
Expand All @@ -50,20 +40,17 @@ async function handleQuestion(question: FormField, page: FormPage, form: FormSch
try {
sanitizeQuestion(question);
setFieldValidators(question);
await transformByType(question);
transformByType(question);
transformByRendering(question);

if (question.questions?.length) {
if (question.type === 'obsGroup' && question.questions.length) {
question.questions.forEach((nestedQuestion) => handleQuestion(nestedQuestion, page, form));
} else {
question.questions = await Promise.all(
question.questions.map((nestedQuestion) => handleQuestion(nestedQuestion, page, form)),
);
question.questions.forEach((nestedQuestion) => handleQuestion(nestedQuestion, page, form));
}
}
question.meta.pageId = page.id;
return question;
} catch (error) {
console.error(error);
}
Expand Down Expand Up @@ -121,7 +108,7 @@ function sanitizeQuestion(question: FormField) {
}
}

function parseBooleanTokenIfPresent(node: any, token: any) {
export function parseBooleanTokenIfPresent(node: any, token: any) {
if (node && typeof node[token] === 'string') {
const trimmed = node[token].trim().toLowerCase();
if (trimmed === 'true' || trimmed === 'false') {
Expand All @@ -145,7 +132,7 @@ function setFieldValidators(question: FormField) {
}
}

async function transformByType(question: FormField) {
function transformByType(question: FormField) {
switch (question.type) {
case 'encounterProvider':
question.questionOptions.rendering = 'encounter-provider';
Expand All @@ -161,9 +148,6 @@ async function transformByType(question: FormField) {
? 'date'
: question.questionOptions.rendering;
break;
case 'personAttribute':
await handlePersonAttributeType(question);
break;
}
}

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

return augmentedQuestions;
}

async function handlePersonAttributeType(question: FormField) {
if (question.questionOptions.rendering !== 'text') {
question.questionOptions.rendering === 'ui-select-extended';
}

const attributeTypeFormat = await getPersonAttributeTypeFormat(question.questionOptions.attributeType);
if (attributeTypeFormat === 'org.openmrs.Location') {
question.questionOptions.datasource = {
name: 'person_attribute_location_datasource',
};
} else if (attributeTypeFormat === 'org.openmrs.Concept') {
question.questionOptions.datasource = {
name: 'select_concept_answers_datasource',
config: {
concept: question.questionOptions?.concept,
},
};
} else if (attributeTypeFormat === 'java.lang.String') {
question.questionOptions.rendering = 'text';
}
}
80 changes: 80 additions & 0 deletions src/transformers/person-attributes-transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { type FormField, type FormSchema, type FormSchemaTransformer, type RenderType, type FormPage } from '../types';
import { getPersonAttributeTypeFormat } from '../api/';
import { parseBooleanTokenIfPresent } from './default-schema-transformer';

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

export const PersonAttributesTransformer: FormSchemaTransformer = {
transform: async (form: FormSchema): Promise<FormSchema> => {
try {
parseBooleanTokenIfPresent(form, 'readonly');
for (const [index, page] of form.pages.entries()) {
const label = page.label ?? '';
page.id = `page-${label.replace(/\s/g, '')}-${index}`;
parseBooleanTokenIfPresent(page, 'readonly');
if (page.sections) {
for (const section of page.sections) {
if (section.questions) {
if (checkQuestions(section.questions)) {
const formMeta = form.meta ?? {};
formMeta.personAttributes = { hasPersonAttributeFields: true };
}
section.questions = await Promise.all(
section.questions.map((question) => handleQuestion(question, page, form)),
);
}
}
}
}
} catch (error) {
console.error('Error in form transformation:', error);
throw error;
}
return form;
},
};

async function handleQuestion(question: FormField, page: FormPage, form: FormSchema): Promise<FormField> {
try {
await transformByType(question);
if (question.questions?.length) {
question.questions = await Promise.all(
question.questions.map((nestedQuestion) => handleQuestion(nestedQuestion, page, form)),
);
}
question.meta.pageId = page.id;
return question;
} catch (error) {
console.error(error);
}
}

async function transformByType(question: FormField) {
switch (question.type) {
case 'personAttribute':
await handlePersonAttributeType(question);
break;
}
}

async function handlePersonAttributeType(question: FormField) {
const attributeTypeFormat = await getPersonAttributeTypeFormat(question.questionOptions.attributeType);
if (attributeTypeFormat === 'org.openmrs.Location') {
question.questionOptions.datasource = {
name: 'location_datasource',
};
} else if (attributeTypeFormat === 'org.openmrs.Concept') {
question.questionOptions.datasource = {
name: 'select_concept_answers_datasource',
config: {
concept: question.questionOptions?.concept,
},
};
} else if (attributeTypeFormat === 'java.lang.String') {
question.questionOptions.rendering = 'text';
}
}

function checkQuestions(questions) {
return questions.some((question) => question.type === 'personAttribute');
}
4 changes: 4 additions & 0 deletions src/types/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export interface FormSchema {
hasProgramFields?: boolean;
[anythingElse: string]: any;
};
personAttributes?: {
hasPersonAttributeFields?: boolean;
[anythingElse: string]: any;
};
};
}

Expand Down

0 comments on commit e967865

Please sign in to comment.