From e967865c5f56bbd9ad1e77ee31c8250c3de0220d Mon Sep 17 00:00:00 2001 From: CynthiaKamau Date: Fri, 13 Dec 2024 17:01:55 +0300 Subject: [PATCH] code review --- .../ui-select-extended.component.tsx | 2 +- src/hooks/useFormJson.ts | 8 +- src/hooks/usePersonAttributes.tsx | 7 +- .../encounter/encounter-form-processor.ts | 15 ++-- .../encounter/encounter-processor-helper.ts | 4 +- .../inbuilt-components/control-templates.ts | 6 -- .../inbuilt-components/inbuiltDataSources.ts | 5 +- .../inbuilt-components/inbuiltTransformers.ts | 5 ++ .../template-component-map.ts | 4 - src/registry/registry.ts | 3 +- .../default-schema-transformer.ts | 78 +++++------------- .../person-attributes-transformer.ts | 80 +++++++++++++++++++ src/types/schema.ts | 4 + 13 files changed, 132 insertions(+), 89 deletions(-) create mode 100644 src/transformers/person-attributes-transformer.ts diff --git a/src/components/inputs/ui-select-extended/ui-select-extended.component.tsx b/src/components/inputs/ui-select-extended/ui-select-extended.component.tsx index f1ded41dd..a7b721f29 100644 --- a/src/components/inputs/ui-select-extended/ui-select-extended.component.tsx +++ b/src/components/inputs/ui-select-extended/ui-select-extended.component.tsx @@ -170,7 +170,7 @@ const UiSelectExtended: React.FC = ({ 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; } diff --git a/src/hooks/useFormJson.ts b/src/hooks/useFormJson.ts index 274a71919..12c1f0698 100644 --- a/src/hooks/useFormJson.ts +++ b/src/hooks/useFormJson.ts @@ -125,10 +125,10 @@ async function refineFormJson( ): Promise { 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); } diff --git a/src/hooks/usePersonAttributes.tsx b/src/hooks/usePersonAttributes.tsx index b65fae98d..138a7bf48 100644 --- a/src/hooks/usePersonAttributes.tsx +++ b/src/hooks/usePersonAttributes.tsx @@ -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>([]); 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) => { diff --git a/src/processors/encounter/encounter-form-processor.ts b/src/processors/encounter/encounter-form-processor.ts index faecff128..f9e9b7b30 100644 --- a/src/processors/encounter/encounter-form-processor.ts +++ b/src/processors/encounter/encounter-form-processor.ts @@ -43,7 +43,10 @@ function useCustomHooks(context: Partial) { 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); @@ -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', @@ -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 diff --git a/src/processors/encounter/encounter-processor-helper.ts b/src/processors/encounter/encounter-processor-helper.ts index 24f1e2ce3..342147508 100644 --- a/src/processors/encounter/encounter-processor-helper.ts +++ b/src/processors/encounter/encounter-processor-helper.ts @@ -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) { diff --git a/src/registry/inbuilt-components/control-templates.ts b/src/registry/inbuilt-components/control-templates.ts index 55b1f34c5..0ea36c84d 100644 --- a/src/registry/inbuilt-components/control-templates.ts +++ b/src/registry/inbuilt-components/control-templates.ts @@ -50,12 +50,6 @@ export const controlTemplates: Array = [ }, }, }, - { - name: 'person-attribute-location', - datasource: { - name: 'person_attribute_location_datasource', - }, - }, ]; export const getControlTemplate = (name: string) => { diff --git a/src/registry/inbuilt-components/inbuiltDataSources.ts b/src/registry/inbuilt-components/inbuiltDataSources.ts index 188107ee0..c437dc423 100644 --- a/src/registry/inbuilt-components/inbuiltDataSources.ts +++ b/src/registry/inbuilt-components/inbuiltDataSources.ts @@ -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 @@ -36,8 +35,8 @@ export const inbuiltDataSources: Array>> = [ component: new EncounterRoleDataSource(), }, { - name: 'person_attribute_location_datasource', - component: new PersonAttributeLocationDataSource(), + name: 'person-attribute-location', + component: new LocationDataSource(), }, ]; diff --git a/src/registry/inbuilt-components/inbuiltTransformers.ts b/src/registry/inbuilt-components/inbuiltTransformers.ts index a35c79cc8..2f67bd743 100644 --- a/src/registry/inbuilt-components/inbuiltTransformers.ts +++ b/src/registry/inbuilt-components/inbuiltTransformers.ts @@ -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'; @@ -7,4 +8,8 @@ export const inbuiltFormTransformers: Array> name: 'DefaultFormSchemaTransformer', component: DefaultFormSchemaTransformer, }, + { + name: 'PersonAttributesTransformer', + component: PersonAttributesTransformer, + }, ]; diff --git a/src/registry/inbuilt-components/template-component-map.ts b/src/registry/inbuilt-components/template-component-map.ts index 8e6b76766..ad0c9064d 100644 --- a/src/registry/inbuilt-components/template-component-map.ts +++ b/src/registry/inbuilt-components/template-component-map.ts @@ -25,8 +25,4 @@ export const templateToComponentMap = [ name: 'encounter-role', baseControlComponent: UiSelectExtended, }, - { - name: 'person_attribute_location_datasource', - baseControlComponent: UiSelectExtended, - }, ]; diff --git a/src/registry/registry.ts b/src/registry/registry.ts index 4b1f7290d..09bcde4da 100644 --- a/src/registry/registry.ts +++ b/src/registry/registry.ts @@ -171,9 +171,10 @@ export async function getRegisteredFieldValueAdapter(type: string): Promise
{ - const transformers: FormSchemaTransformer[] = []; + const transformers: Array = []; const cachedTransformers = registryCache.formSchemaTransformers; + if (Object.keys(cachedTransformers).length) { return Object.values(cachedTransformers); } diff --git a/src/transformers/default-schema-transformer.ts b/src/transformers/default-schema-transformer.ts index 9019d3fc6..36f78f5c9 100644 --- a/src/transformers/default-schema-transformer.ts +++ b/src/transformers/default-schema-transformer.ts @@ -2,36 +2,26 @@ 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 => { - 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); } @@ -39,7 +29,7 @@ export const DefaultFormSchemaTransformer: FormSchemaTransformer = { }, }; -async function handleQuestion(question: FormField, page: FormPage, form: FormSchema): Promise { +function handleQuestion(question: FormField, page: FormPage, form: FormSchema) { if (question.type === 'programState') { const formMeta = form.meta ?? {}; formMeta.programs = formMeta.programs @@ -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); } @@ -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') { @@ -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'; @@ -161,9 +148,6 @@ async function transformByType(question: FormField) { ? 'date' : question.questionOptions.rendering; break; - case 'personAttribute': - await handlePersonAttributeType(question); - break; } } @@ -292,25 +276,3 @@ function handleQuestionsWithObsComments(sectionQuestions: Array): 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'; - } -} diff --git a/src/transformers/person-attributes-transformer.ts b/src/transformers/person-attributes-transformer.ts new file mode 100644 index 000000000..d81f371e4 --- /dev/null +++ b/src/transformers/person-attributes-transformer.ts @@ -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 => { + 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 { + 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'); +} diff --git a/src/types/schema.ts b/src/types/schema.ts index aca5d7526..da67972eb 100644 --- a/src/types/schema.ts +++ b/src/types/schema.ts @@ -25,6 +25,10 @@ export interface FormSchema { hasProgramFields?: boolean; [anythingElse: string]: any; }; + personAttributes?: { + hasPersonAttributeFields?: boolean; + [anythingElse: string]: any; + }; }; }