-
Notifications
You must be signed in to change notification settings - Fork 66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
(feat) O3-3367 Add support for person attributes #423
base: main
Are you sure you want to change the base?
Conversation
Size Change: -264 kB (-17.31%) 👏 Total Size: 1.26 MB
ℹ️ View Unchanged
|
be9d967
to
48be05c
Compare
7b25040
to
ebf119f
Compare
ebf119f
to
f95290b
Compare
So the immediate thing I see here is that we're embedding "format" in the form. I don't really love when we duplicate metadata like this in forms. Just load the format from the backend definition of the attribute type, so we don't face weird issues where changing an attribute type causes forms to break. |
8ac443b
to
e0f7bcc
Compare
@@ -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> | FormSchema; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need the Promise here? i believe the form is resolved before we get here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc: @samuelmale
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think she explained here: https://github.com/openmrs/openmrs-esm-form-engine-lib/pull/423/files#r1863144624
src/types/schema.ts
Outdated
attribute?: { | ||
type?: string; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the ampath schema docs for personAtttribute, we could eliminate the need for this being an object for backwards compatibility as well have a single format unless you have reasons for going this route
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me work on this
} | ||
field.meta.submission.newValue = { | ||
value: value, | ||
attributeType: field.questionOptions?.attribute?.type, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The suggested changes in the schema will trickle down
src/hooks/useFormJson.tsx
Outdated
formJson: any, | ||
schemaTransformers: FormSchemaTransformer[] = [], | ||
formSessionIntent?: string, | ||
): FormSchema { | ||
): Promise<FormSchema> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have reservations to the Promise additions to this file because this affects the way the forms load. Maybe you can share why we need this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This adjustment is needed because the format of a person attribute uuid can be a text
, location
or concept
and those details are fetched from the getPersonAttributeTypeFormat
endpoint. Based on the response, we transform the question to the right datasource. @ibacher recommended that we get the backend definitions incase they change, we will still always get the right one. You can add suggestions on how to better handle it incase there is a better way to do it.
e0f7bcc
to
e9aa02e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work @CynthiaKamau! Any chance we can have some integrational test cases covering all supported rendering types?
export const PersonAttributesAdapter: FormFieldValueAdapter = { | ||
transformFieldValue: function (field: FormField, value: any, context: FormContextProps) { | ||
clearSubmission(field); | ||
if (field.meta?.previousValue?.value === value || isEmpty(value)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if the user tries to delete an attribute in edit mode?
if (field.meta?.previousValue && isEmpty(value)) {
// should we void the attribute?
}
import { clearSubmission } from '../utils/common-utils'; | ||
import { isEmpty } from '../validators/form-validator'; | ||
|
||
export const PersonAttributesAdapter: FormFieldValueAdapter = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you planning on adding some test coverage for this adapter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes i will
src/hooks/useFormJson.tsx
Outdated
for (let transformer of schemaTransformers) { | ||
const draftForm = await transformer.transform(formJson); | ||
formJson = draftForm; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ibacher Do you see any benefits from executing these sequentially?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, merging things would be hard, but this code actually has very different semantics from the previous version. Might be better to preserve the same semantics:
for (let transformer of schemaTransformers) { | |
const draftForm = await transformer.transform(formJson); | |
formJson = draftForm; | |
} | |
schemaTransformers.reduce((form, transformer) => Promise.resolve(transformer.transform(await form)), Promise.resolve(formJson)); |
src/hooks/usePersonAttributes.tsx
Outdated
const [error, setError] = useState(null); | ||
|
||
useEffect(() => { | ||
if (patientUuid) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you conditionally load attributes only for forms that support them? (Here is an example https://github.com/openmrs/openmrs-esm-form-engine-lib/blob/main/src/hooks/usePatientPrograms.ts#L12)
@@ -162,6 +168,27 @@ export class EncounterFormProcessor extends FormProcessor { | |||
}); | |||
} | |||
|
|||
// save person attributes | |||
try { | |||
const personattributes = preparePersonAttributes(context.formFields, context.location?.uuid); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const personattributes = preparePersonAttributes(context.formFields, context.location?.uuid); | |
const personAttributes = preparePersonAttributes(context.formFields, context.location?.uuid); |
// save person attributes | ||
try { | ||
const personattributes = preparePersonAttributes(context.formFields, context.location?.uuid); | ||
const savedPrograms = await savePersonAttributes(context.patient, personattributes); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const savedPrograms = await savePersonAttributes(context.patient, personattributes); | |
const savedAttributes = await savePersonAttributes(context.patient, personAttributes); |
@@ -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> | FormSchema; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think she explained here: https://github.com/openmrs/openmrs-esm-form-engine-lib/pull/423/files#r1863144624
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; | ||
import { BaseOpenMRSDataSource } from './data-source'; | ||
|
||
export class PersonAttributeLocationDataSource extends BaseOpenMRSDataSource { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export class PersonAttributeLocationDataSource extends BaseOpenMRSDataSource { | |
export class LocationAttributeDataSource extends BaseOpenMRSDataSource { |
async fetchData(searchTerm: string, config?: Record<string, any>, uuid?: string): Promise<any[]> { | ||
const rep = 'v=custom:(uuid,display)'; | ||
const url = `${restBaseUrl}/location?${rep}`; | ||
const { data } = await openmrsFetch(searchTerm ? `${url}&q=${searchTerm}` : url); | ||
|
||
return data?.results; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This data source seems to just load locations (not tied down to the "attribute" model). Anything stopping you from reusing the existing location DS`?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^^^^ This
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@@ -267,3 +283,25 @@ function handleQuestionsWithObsComments(sectionQuestions: Array<FormField>): Arr | |||
|
|||
return augmentedQuestions; | |||
} | |||
|
|||
async function handlePersonAttributeType(question: FormField) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you planning on adding some test coverage?
@@ -149,7 +149,7 @@ const UiSelectExtended: React.FC<FormFieldInputProps> = ({ 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)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this required for this PR?
async fetchData(searchTerm: string, config?: Record<string, any>, uuid?: string): Promise<any[]> { | ||
const rep = 'v=custom:(uuid,display)'; | ||
const url = `${restBaseUrl}/location?${rep}`; | ||
const { data } = await openmrsFetch(searchTerm ? `${url}&q=${searchTerm}` : url); | ||
|
||
return data?.results; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^^^^ This
@@ -7,7 +7,7 @@ export class SelectConceptAnswersDatasource extends BaseOpenMRSDataSource { | |||
} | |||
|
|||
fetchData(searchTerm: string, config?: Record<string, any>): Promise<any[]> { | |||
const apiUrl = this.url.replace('conceptUuid', config.referencedValue || config.concept); | |||
const apiUrl = this.url.replace('conceptUuid', config.concept || config.referencedValue); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the inversion here?
src/hooks/useFormJson.tsx
Outdated
for (let transformer of schemaTransformers) { | ||
const draftForm = await transformer.transform(formJson); | ||
formJson = draftForm; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, merging things would be hard, but this code actually has very different semantics from the previous version. Might be better to preserve the same semantics:
for (let transformer of schemaTransformers) { | |
const draftForm = await transformer.transform(formJson); | |
formJson = draftForm; | |
} | |
schemaTransformers.reduce((form, transformer) => Promise.resolve(transformer.transform(await form)), Promise.resolve(formJson)); |
src/hooks/usePersonAttributes.tsx
Outdated
if (patientUuid) { | ||
openmrsFetch(`${restBaseUrl}/patient/${patientUuid}?v=custom:(attributes)`) | ||
.then((response) => { | ||
setPersonAttributes(response?.data?.attributes); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setPersonAttributes(response?.data?.attributes); | |
setPersonAttributes(response.data?.attributes); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there some reason we're not using SWR here?
@@ -94,6 +94,6 @@ export const inbuiltControls: Array<RegistryItem<React.ComponentType<FormFieldIn | |||
}, | |||
...controlTemplates.map((template) => ({ | |||
name: template.name, | |||
component: templateToComponentMap.find((component) => component.name === template.name).baseControlComponent, | |||
component: templateToComponentMap.find((component) => component.name === template.name)?.baseControlComponent, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't really understand why we need the ?
here?
src/registry/registry.ts
Outdated
@@ -171,7 +171,7 @@ export async function getRegisteredFieldValueAdapter(type: string): Promise<Form | |||
} | |||
|
|||
export async function getRegisteredFormSchemaTransformers(): Promise<FormSchemaTransformer[]> { | |||
const transformers = []; | |||
const transformers: FormSchemaTransformer[] = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const transformers: FormSchemaTransformer[] = []; | |
const transformers: Array<FormSchemaTransformer> = []; |
|
||
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; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we refactor this? The code is way more complicated than it needs to be and has way more iterations than it needs to. For example:
transformers = transformers.concat(inbuildFormTransformers.map((transformer) => transformer.component ? ({...transformer, component: Promise.resolve(transformer.component)}) : null).filter(Boolean).map(async (transformer) => {
const theTransformer = await transformer.component;
registryCache.formSchemaTransformers[transformer.name] = theTransformer;
return theTransformer;
}));
@@ -125,7 +138,7 @@ function setFieldValidators(question: FormField) { | |||
} | |||
} | |||
|
|||
function transformByType(question: FormField) { | |||
async function transformByType(question: FormField) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually think there's a good argument to be made to separate this out from the DefaultFormSchemaTransformer. This is inherently async whereas nothing in the default transformer otherwise is. Also, for handling attributes, it would be substantially better to batch them up into a single request... or at least to avoid re-requesting an attribute type already loaded, but that logic requires a different structure than the default class provides.
if (question.questionOptions.rendering !== 'text') { | ||
question.questionOptions.rendering === 'ui-select-extended'; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this does what you wanted it to do.
9b355d5
to
e967865
Compare
e967865
to
cb6a495
Compare
Requirements
Summary
Add support for person attributes
Schema :
Screenshots
Screen.Recording.2024-11-21.at.13.54.06.mov
Related Issue
Other