diff --git a/src/adapters/person-attributes-adapter.ts b/src/adapters/person-attributes-adapter.ts new file mode 100644 index 000000000..2331fd7bd --- /dev/null +++ b/src/adapters/person-attributes-adapter.ts @@ -0,0 +1,54 @@ +import { type PersonAttribute, type OpenmrsResource } from '@openmrs/esm-framework'; +import { type FormContextProps } from '../provider/form-provider'; +import { type FormField, type FormFieldValueAdapter, type FormProcessorContextProps } from '../types'; +import { clearSubmission } from '../utils/common-utils'; +import { isEmpty } from '../validators/form-validator'; + +export const PersonAttributesAdapter: FormFieldValueAdapter = { + transformFieldValue: function (field: FormField, value: any, context: FormContextProps) { + clearSubmission(field); + if (field.meta?.previousValue?.value === value || isEmpty(value)) { + return null; + } + field.meta.submission.newValue = { + value: value, + attributeType: field.questionOptions?.attribute?.type, + }; + return field.meta.submission.newValue; + }, + getInitialValue: function (field: FormField, sourceObject: OpenmrsResource, context: FormProcessorContextProps) { + const rendering = field.questionOptions.rendering; + + const personAttributeValue = context?.customDependencies.personAttributes.find( + (attribute: PersonAttribute) => attribute.attributeType.uuid === field.questionOptions.attribute?.type, + )?.value; + if (rendering === 'text') { + if (typeof personAttributeValue === 'string') { + return personAttributeValue; + } else if ( + personAttributeValue && + typeof personAttributeValue === 'object' && + 'display' in personAttributeValue + ) { + return personAttributeValue?.display; + } + } else if (rendering === 'ui-select-extended') { + if (personAttributeValue && typeof personAttributeValue === 'object' && 'uuid' in personAttributeValue) { + return personAttributeValue?.uuid; + } + } + return null; + }, + getPreviousValue: function (field: FormField, sourceObject: OpenmrsResource, context: FormProcessorContextProps) { + return null; + }, + getDisplayValue: function (field: FormField, value: any) { + if (value?.display) { + return value.display; + } + return value; + }, + tearDown: function (): void { + return; + }, +}; diff --git a/src/api/index.ts b/src/api/index.ts index 8124c4df5..7e49d0a05 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,4 +1,4 @@ -import { fhirBaseUrl, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; +import { fhirBaseUrl, openmrsFetch, type PersonAttribute, restBaseUrl } from '@openmrs/esm-framework'; import { encounterRepresentation } from '../constants'; import { type OpenmrsForm, type PatientIdentifier, type PatientProgramPayload } from '../types'; import { isUuid } from '../utils/boolean-utils'; @@ -180,3 +180,36 @@ export function savePatientIdentifier(patientIdentifier: PatientIdentifier, pati body: JSON.stringify(patientIdentifier), }); } + +export function savePersonAttribute(personAttribute: PersonAttribute, personUuid: string) { + let url: string; + + if (personAttribute.uuid) { + url = `${restBaseUrl}/person/${personUuid}/attribute/${personAttribute.uuid}`; + } else { + url = `${restBaseUrl}/person/${personUuid}/attribute`; + } + + return openmrsFetch(url, { + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify(personAttribute), + }); +} + +export async function getPersonAttributeTypeFormat(personAttributeTypeUuid: string) { + try { + const response = await openmrsFetch( + `${restBaseUrl}/personattributetype/${personAttributeTypeUuid}?v=custom:(format)`, + ); + if (response) { + const { data } = response; + return data?.format; + } + return null; + } catch (error) { + return null; + } +} 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 bd8df9e65..0af52c578 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 @@ -149,7 +149,7 @@ const UiSelectExtended: React.FC = ({ field, errors, warnin selectedItem={selectedItem} placeholder={isSearchable ? t('search', 'Search') + '...' : null} shouldFilterItem={({ item, inputValue }) => { - if (!inputValue) { + if (!inputValue || items.find((item) => item.uuid == field.value)) { // Carbon's initial call at component mount return true; } diff --git a/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx b/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx index acc134043..9c00b395b 100644 --- a/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx +++ b/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx @@ -107,6 +107,14 @@ jest.mock('../../../registry/registry', () => { }; }); +jest.mock('../../../hooks/usePersonAttributes', () => ({ + usePersonAttributes: jest.fn().mockReturnValue({ + personAttributes: [], + error: null, + isLoading: false, + }), +})); + const encounter = { uuid: 'encounter-uuid', obs: [ diff --git a/src/components/inputs/unspecified/unspecified.test.tsx b/src/components/inputs/unspecified/unspecified.test.tsx index d7635e558..daf37242d 100644 --- a/src/components/inputs/unspecified/unspecified.test.tsx +++ b/src/components/inputs/unspecified/unspecified.test.tsx @@ -58,6 +58,14 @@ jest.mock('../../../hooks/useEncounter', () => ({ }), })); +jest.mock('../../../hooks/usePersonAttributes', () => ({ + usePersonAttributes: jest.fn().mockReturnValue({ + personAttributes: [], + error: null, + isLoading: false, + }), +})); + const renderForm = async (mode: SessionMode = 'enter') => { await act(async () => { render( diff --git a/src/datasources/person-attribute-datasource.ts b/src/datasources/person-attribute-datasource.ts new file mode 100644 index 000000000..a122b76f7 --- /dev/null +++ b/src/datasources/person-attribute-datasource.ts @@ -0,0 +1,16 @@ +import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; +import { BaseOpenMRSDataSource } from './data-source'; + +export class PersonAttributeLocationDataSource extends BaseOpenMRSDataSource { + constructor() { + super(null); + } + + async fetchData(searchTerm: string, config?: Record, uuid?: string): Promise { + const rep = 'v=custom:(uuid,display)'; + const url = `${restBaseUrl}/location?${rep}`; + const { data } = await openmrsFetch(searchTerm ? `${url}&q=${searchTerm}` : url); + + return data?.results; + } +} diff --git a/src/datasources/select-concept-answers-datasource.ts b/src/datasources/select-concept-answers-datasource.ts index 03504ac48..208946550 100644 --- a/src/datasources/select-concept-answers-datasource.ts +++ b/src/datasources/select-concept-answers-datasource.ts @@ -7,7 +7,7 @@ export class SelectConceptAnswersDatasource extends BaseOpenMRSDataSource { } fetchData(searchTerm: string, config?: Record): Promise { - const apiUrl = this.url.replace('conceptUuid', config.referencedValue || config.concept); + const apiUrl = this.url.replace('conceptUuid', config.concept || config.referencedValue); return openmrsFetch(apiUrl).then(({ data }) => { return data['setMembers'].length ? data['setMembers'] : data['answers']; }); diff --git a/src/form-engine.test.tsx b/src/form-engine.test.tsx index 4e60fc2d0..bd1685d2b 100644 --- a/src/form-engine.test.tsx +++ b/src/form-engine.test.tsx @@ -118,6 +118,14 @@ jest.mock('./hooks/useConcepts', () => ({ }), })); +jest.mock('./hooks/usePersonAttributes', () => ({ + usePersonAttributes: jest.fn().mockReturnValue({ + personAttributes: [], + error: null, + isLoading: false, + }), +})); + describe('Form engine component', () => { const user = userEvent.setup(); diff --git a/src/hooks/useFormJson.tsx b/src/hooks/useFormJson.tsx index 0f25f898d..da7e3c992 100644 --- a/src/hooks/useFormJson.tsx +++ b/src/hooks/useFormJson.tsx @@ -108,14 +108,17 @@ function validateFormsArgs(formUuid: string, rawFormJson: any): Error { * @param {string} [formSessionIntent] - The optional form session intent. * @returns {FormSchema} - The refined form JSON object of type FormSchema. */ -function refineFormJson( +async function refineFormJson( formJson: any, schemaTransformers: FormSchemaTransformer[] = [], formSessionIntent?: string, -): FormSchema { +): Promise { removeInlineSubForms(formJson, formSessionIntent); // apply form schema transformers - schemaTransformers.reduce((draftForm, transformer) => transformer.transform(draftForm), formJson); + for (let transformer of schemaTransformers) { + const draftForm = await transformer.transform(formJson); + formJson = draftForm; + } setEncounterType(formJson); return applyFormIntent(formSessionIntent, formJson); } @@ -134,7 +137,7 @@ function parseFormJson(formJson: any): FormSchema { * @param {FormSchema} formJson - The input form JSON object of type FormSchema. * @param {string} formSessionIntent - The form session intent. */ -function removeInlineSubForms(formJson: FormSchema, formSessionIntent: string): void { +async function removeInlineSubForms(formJson: FormSchema, formSessionIntent: string): Promise { for (let i = formJson.pages.length - 1; i >= 0; i--) { const page = formJson.pages[i]; if ( @@ -143,7 +146,7 @@ function removeInlineSubForms(formJson: FormSchema, formSessionIntent: string): page.subform?.form?.encounterType === formJson.encounterType ) { const nonSubformPages = page.subform.form.pages.filter((page) => !isTrue(page.isSubform)); - formJson.pages.splice(i, 1, ...refineFormJson(page.subform.form, [], formSessionIntent).pages); + formJson.pages.splice(i, 1, ...(await refineFormJson(page.subform.form, [], formSessionIntent)).pages); } } } diff --git a/src/hooks/usePersonAttributes.tsx b/src/hooks/usePersonAttributes.tsx new file mode 100644 index 000000000..b65fae98d --- /dev/null +++ b/src/hooks/usePersonAttributes.tsx @@ -0,0 +1,30 @@ +import { openmrsFetch, type PersonAttribute, restBaseUrl } from '@openmrs/esm-framework'; +import { useEffect, useState } from 'react'; + +export const usePersonAttributes = (patientUuid: string) => { + const [personAttributes, setPersonAttributes] = useState>([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (patientUuid) { + openmrsFetch(`${restBaseUrl}/patient/${patientUuid}?v=custom:(attributes)`) + .then((response) => { + setPersonAttributes(response?.data?.attributes); + setIsLoading(false); + }) + .catch((error) => { + setError(error); + setIsLoading(false); + }); + } else { + setIsLoading(false); + } + }, [patientUuid]); + + return { + personAttributes, + error, + isLoading: isLoading, + }; +}; diff --git a/src/processors/encounter/encounter-form-processor.ts b/src/processors/encounter/encounter-form-processor.ts index 9c6f36f29..94e02a006 100644 --- a/src/processors/encounter/encounter-form-processor.ts +++ b/src/processors/encounter/encounter-form-processor.ts @@ -19,9 +19,11 @@ import { prepareEncounter, preparePatientIdentifiers, preparePatientPrograms, + preparePersonAttributes, saveAttachments, savePatientIdentifiers, savePatientPrograms, + savePersonAttributes, } from './encounter-processor-helper'; import { type OpenmrsResource, showSnackbar, translateFrom } from '@openmrs/esm-framework'; import { moduleName } from '../../globals'; @@ -31,6 +33,7 @@ import { useEncounterRole } from '../../hooks/useEncounterRole'; import { evaluateAsyncExpression, type FormNode } from '../../utils/expression-runner'; import { hasRendering } from '../../utils/common-utils'; import { extractObsValueAndDisplay } from '../../utils/form-helper'; +import { usePersonAttributes } from '../../hooks/usePersonAttributes'; function useCustomHooks(context: Partial) { const [isLoading, setIsLoading] = useState(true); @@ -40,13 +43,14 @@ function useCustomHooks(context: Partial) { context.patient?.id, context.formJson, ); + const { isLoading: isLoadingPersonAttributes, personAttributes } = usePersonAttributes(context.patient?.id); useEffect(() => { - setIsLoading(isLoadingPatientPrograms || isLoadingEncounter || isLoadingEncounterRole); - }, [isLoadingPatientPrograms, isLoadingEncounter, isLoadingEncounterRole]); + setIsLoading(isLoadingPatientPrograms || isLoadingEncounter || isLoadingEncounterRole || isLoadingPersonAttributes); + }, [isLoadingPatientPrograms, isLoadingEncounter, isLoadingEncounterRole, isLoadingPersonAttributes]); return { - data: { encounter, patientPrograms, encounterRole }, + data: { encounter, patientPrograms, encounterRole, personAttributes }, isLoading, error: null, updateContext: (setContext: React.Dispatch>) => { @@ -59,6 +63,7 @@ function useCustomHooks(context: Partial) { ...context.customDependencies, patientPrograms: patientPrograms, defaultEncounterRole: encounterRole, + personAttributes: personAttributes, }, }; }); @@ -79,6 +84,7 @@ const contextInitializableTypes = [ 'patientIdentifier', 'encounterRole', 'programState', + 'personAttributes', ]; export class EncounterFormProcessor extends FormProcessor { @@ -162,6 +168,27 @@ 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) { + showSnackbar({ + title: translateFn('personAttributesSaved', 'Person attribute(s) saved successfully'), + kind: 'success', + isLowContrast: true, + }); + } + } catch (error) { + const errorMessages = extractErrorMessagesFromResponse(error); + return Promise.reject({ + title: translateFn('errorSavingPersonAttributes', 'Error saving person attributes'), + description: errorMessages.join(', '), + kind: 'error', + critical: true, + }); + } + // save encounter try { const { data: savedEncounter } = await saveEncounter(abortController, encounter, encounter.uuid); diff --git a/src/processors/encounter/encounter-processor-helper.ts b/src/processors/encounter/encounter-processor-helper.ts index d4fbeb7d1..0e5ebd643 100644 --- a/src/processors/encounter/encounter-processor-helper.ts +++ b/src/processors/encounter/encounter-processor-helper.ts @@ -7,7 +7,7 @@ import { type PatientProgram, type PatientProgramPayload, } from '../../types'; -import { saveAttachment, savePatientIdentifier, saveProgramEnrollment } from '../../api'; +import { saveAttachment, savePatientIdentifier, savePersonAttribute, saveProgramEnrollment } from '../../api'; import { hasRendering, hasSubmission } from '../../utils/common-utils'; import dayjs from 'dayjs'; import { assignedObsIds, constructObs, voidObs } from '../../adapters/obs-adapter'; @@ -16,6 +16,7 @@ import { ConceptTrue } from '../../constants'; import { DefaultValueValidator } from '../../validators/default-value-validator'; import { cloneRepeatField } from '../../components/repeat/helpers'; import { assignedOrderIds } from '../../adapters/orders-adapter'; +import { type PersonAttribute } from '@openmrs/esm-framework'; export function prepareEncounter( context: FormContextProps, @@ -152,6 +153,12 @@ export function saveAttachments(fields: FormField[], encounter: OpenmrsEncounter }); } +export function savePersonAttributes(patient: fhir.Patient, attributes: PersonAttribute[]) { + return attributes.map((personAttribute) => { + return savePersonAttribute(personAttribute, patient.id); + }); +} + export function getMutableSessionProps(context: FormContextProps) { const { formFields, @@ -328,3 +335,9 @@ export async function hydrateRepeatField( }), ).then((results) => results.flat()); } + +export function preparePersonAttributes(fields: FormField[], encounterLocation: string): PersonAttribute[] { + return fields + .filter((field) => field.type === 'personAttribute' && hasSubmission(field)) + .map((field) => field.meta.submission.newValue); +} diff --git a/src/registry/inbuilt-components/control-templates.ts b/src/registry/inbuilt-components/control-templates.ts index 0ea36c84d..55b1f34c5 100644 --- a/src/registry/inbuilt-components/control-templates.ts +++ b/src/registry/inbuilt-components/control-templates.ts @@ -50,6 +50,12 @@ 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/inbuiltControls.ts b/src/registry/inbuilt-components/inbuiltControls.ts index 4e4a80938..fa005a6ad 100644 --- a/src/registry/inbuilt-components/inbuiltControls.ts +++ b/src/registry/inbuilt-components/inbuiltControls.ts @@ -94,6 +94,6 @@ export const inbuiltControls: Array ({ name: template.name, - component: templateToComponentMap.find((component) => component.name === template.name).baseControlComponent, + component: templateToComponentMap.find((component) => component.name === template.name)?.baseControlComponent, })), ]; diff --git a/src/registry/inbuilt-components/inbuiltDataSources.ts b/src/registry/inbuilt-components/inbuiltDataSources.ts index 0a1d92492..188107ee0 100644 --- a/src/registry/inbuilt-components/inbuiltDataSources.ts +++ b/src/registry/inbuilt-components/inbuiltDataSources.ts @@ -5,6 +5,7 @@ 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 @@ -34,6 +35,10 @@ export const inbuiltDataSources: Array>> = [ name: 'encounter_role_datasource', component: new EncounterRoleDataSource(), }, + { + name: 'person_attribute_location_datasource', + component: new PersonAttributeLocationDataSource(), + }, ]; export const validateInbuiltDatasource = (name: string) => { diff --git a/src/registry/inbuilt-components/inbuiltFieldValueAdapters.ts b/src/registry/inbuilt-components/inbuiltFieldValueAdapters.ts index 9a4c3d850..c4d62bd37 100644 --- a/src/registry/inbuilt-components/inbuiltFieldValueAdapters.ts +++ b/src/registry/inbuilt-components/inbuiltFieldValueAdapters.ts @@ -11,6 +11,7 @@ import { OrdersAdapter } from '../../adapters/orders-adapter'; import { PatientIdentifierAdapter } from '../../adapters/patient-identifier-adapter'; import { ProgramStateAdapter } from '../../adapters/program-state-adapter'; import { type FormFieldValueAdapter } from '../../types'; +import { PersonAttributesAdapter } from '../../adapters/person-attributes-adapter'; export const inbuiltFieldValueAdapters: RegistryItem[] = [ { @@ -61,4 +62,8 @@ export const inbuiltFieldValueAdapters: RegistryItem[] = type: 'patientIdentifier', component: PatientIdentifierAdapter, }, + { + type: 'personAttribute', + component: PersonAttributesAdapter, + }, ]; diff --git a/src/registry/inbuilt-components/template-component-map.ts b/src/registry/inbuilt-components/template-component-map.ts index ad0c9064d..8e6b76766 100644 --- a/src/registry/inbuilt-components/template-component-map.ts +++ b/src/registry/inbuilt-components/template-component-map.ts @@ -25,4 +25,8 @@ 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 1d96bb8b8..4b1f7290d 100644 --- a/src/registry/registry.ts +++ b/src/registry/registry.ts @@ -171,7 +171,7 @@ export async function getRegisteredFieldValueAdapter(type: string): Promise
{ - const transformers = []; + const transformers: FormSchemaTransformer[] = []; const cachedTransformers = registryCache.formSchemaTransformers; if (Object.keys(cachedTransformers).length) { @@ -186,14 +186,21 @@ export async function getRegisteredFormSchemaTransformers(): Promise transformer !== undefined)); - - transformers.push(...inbuiltFormTransformers.map((inbuiltTransformer) => inbuiltTransformer.component)); - + const inbuiltTransformersPromises = inbuiltFormTransformers.map(async (inbuiltTransformer) => { + const transformer = inbuiltTransformer.component; + if (transformer instanceof Promise) { + return await transformer; + } + return transformer; + }); + const resolvedInbuiltTransformers = await Promise.all(inbuiltTransformersPromises); + transformers.push(...resolvedInbuiltTransformers); transformers.forEach((transformer) => { const inbuiltTransformer = inbuiltFormTransformers.find((t) => t.component === transformer); - registryCache.formSchemaTransformers[inbuiltTransformer.name] = transformer; + if (inbuiltTransformer) { + registryCache.formSchemaTransformers[inbuiltTransformer.name] = transformer; + } }); - return transformers; } diff --git a/src/transformers/default-schema-transformer.test.ts b/src/transformers/default-schema-transformer.test.ts index 7ec73a7be..c7cbd3fc8 100644 --- a/src/transformers/default-schema-transformer.test.ts +++ b/src/transformers/default-schema-transformer.test.ts @@ -158,11 +158,11 @@ const expectedTransformedSchema = { }; describe('Default form schema transformer', () => { - it('should transform AFE schema to be compatible with RFE', () => { - expect(DefaultFormSchemaTransformer.transform(testForm as any)).toEqual(expectedTransformedSchema); + it('should transform AFE schema to be compatible with RFE', async () => { + expect(await DefaultFormSchemaTransformer.transform(testForm as any)).toEqual(expectedTransformedSchema); }); - it('should handle checkbox-searchable rendering', () => { + it('should handle checkbox-searchable rendering', async () => { // setup const form = { pages: [ @@ -185,7 +185,7 @@ describe('Default form schema transformer', () => { ], }; // exercise - const transformedForm = DefaultFormSchemaTransformer.transform(form as FormSchema); + const transformedForm = await DefaultFormSchemaTransformer.transform(form as FormSchema); const transformedQuestion = transformedForm.pages[0].sections[0].questions[0]; // verify expect(transformedQuestion.questionOptions.rendering).toEqual('checkbox'); diff --git a/src/transformers/default-schema-transformer.ts b/src/transformers/default-schema-transformer.ts index c8ad91e3f..cf604fead 100644 --- a/src/transformers/default-schema-transformer.ts +++ b/src/transformers/default-schema-transformer.ts @@ -1,26 +1,36 @@ 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: (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)); - }); + 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)), + ); + } + } + } } - }); + } catch (error) { + console.error('Error in form transformation:', error); + throw error; + } if (form.meta?.programs) { handleProgramMetaTags(form); } @@ -28,7 +38,7 @@ export const DefaultFormSchemaTransformer: FormSchemaTransformer = { }, }; -function handleQuestion(question: FormField, page: FormPage, form: FormSchema) { +async function handleQuestion(question: FormField, page: FormPage, form: FormSchema): Promise { if (question.type === 'programState') { const formMeta = form.meta ?? {}; formMeta.programs = formMeta.programs @@ -39,17 +49,20 @@ function handleQuestion(question: FormField, page: FormPage, form: FormSchema) { try { sanitizeQuestion(question); setFieldValidators(question); - transformByType(question); + await 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.forEach((nestedQuestion) => handleQuestion(nestedQuestion, page, form)); + 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); } @@ -125,7 +138,7 @@ function setFieldValidators(question: FormField) { } } -function transformByType(question: FormField) { +async function transformByType(question: FormField) { switch (question.type) { case 'encounterProvider': question.questionOptions.rendering = 'encounter-provider'; @@ -141,6 +154,9 @@ function transformByType(question: FormField) { ? 'date' : question.questionOptions.rendering; break; + case 'personAttribute': + await handlePersonAttributeType(question); + break; } } @@ -267,3 +283,26 @@ 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?.attribute?.type); + + 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/types/index.ts b/src/types/index.ts index 39434ae29..2f58a5104 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -102,7 +102,7 @@ export interface FormSchemaTransformer { /** * Transforms the raw schema to be compatible with the React Form Engine. */ - transform: (form: FormSchema) => FormSchema; + transform: (form: FormSchema) => Promise | FormSchema; } export interface PostSubmissionAction { diff --git a/src/types/schema.ts b/src/types/schema.ts index 3226e39e7..2fd13adee 100644 --- a/src/types/schema.ts +++ b/src/types/schema.ts @@ -184,6 +184,9 @@ export interface FormQuestionOptions { comment?: string; orientation?: 'vertical' | 'horizontal'; shownCommentOptions?: { validators?: Array>; hide?: { hideWhenExpression: string } }; + attribute?: { + type?: string; + }; } export interface QuestionAnswerOption {