Skip to content
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-1911: Reference Mappings instead of Concept UUIDs #86

Merged
merged 3 commits into from
Jun 2, 2023

Conversation

mogoodrich
Copy link
Member

(feat) O3-1911: Reference Mappings instead of Concept UUIDs
(feat) O3-2129: Form Engine: Support Default Labels form obs questions and answers

Requirements

  • This PR has a title that briefly describes the work done including the ticket number. If there is a ticket, make sure your PR title includes a conventional commit label. See existing PR titles for inspiration.
  • My work conforms to the OpenMRS 3.0 Styleguide and design documentation.
  • My work includes tests or is validated by existing tests.

Summary

Screenshots

Related Issue

Other

(feat) O3-2129: Form Engine: Support Default Labels form obs questions and answers
(feat) O3-2129: Form Engine: Support Default Labels form obs questions and answers
@mogoodrich mogoodrich marked this pull request as draft May 26, 2023 19:54
@github-actions
Copy link

github-actions bot commented May 26, 2023

Size Change: -34.5 kB (-6%) ✅

Total Size: 548 kB

Filename Size Change
dist/692.js 0 B -34.5 kB (removed) 🏆
ℹ️ View Unchanged
Filename Size Change
dist/225.js 9.82 kB 0 B
dist/272.js 6.59 kB 0 B
dist/290.js 81.1 kB 0 B
dist/294.js 2.63 kB 0 B
dist/366.js 6.94 kB 0 B
dist/591.js 2.96 kB 0 B
dist/595.js 106 kB 0 B
dist/634.js 821 B 0 B
dist/70.js 34.9 kB 0 B
dist/709.js 2.42 kB 0 B
dist/986.js 193 kB 0 B
dist/main.js 97.6 kB -8 B (0%)
dist/openmrs-form-engine-lib.js 3.24 kB -5 B (0%)

compressed-size-action

@mogoodrich
Copy link
Member Author

@samuelmale @ibacher @denniskigen @donaldkibet

I put this PR in draft form but it's not ready for merge yet, but I'd still be interested in your review, because as I told @ibacher I'm currently in the state where I know "just enough to be dangerous" and would love your thoughts on the approach.

This is meant to handle the linked tickets and support:

  • Referencing concepts by mapping (ie "CIEL:1234") instead of uuid
  • Using the "display" string of a concept if no label is specified

You can see in the code, but when rendering a form, we now pull out all concept references from the form schema and fetch those concepts from the backend, and the swap out any form references and labels as neeeded when setting up the fields.

It is working in my simple testing, but I should do some more advanced tests, and also should look into creating unit tests around this functionality.

Some high-level questions that I had:

  • Is this library currently in active use, or is it still a work-in-progress? Just wondering because there's a much higher level of testing required on my end if this in active use :)
  • I haven't looked into how the engine renders previously entered forms/encounters, so I need to confirm that this new functionality is compatible with/doesn't break any functionality that maps existing obs back to form fields.
  • I haven't evaluated any performance hit this functionality may have
  • On a related note, is there anything particular I need to do to make this work with offline mode? (does the forrm engine work with offline mode, for that matter?)
  • What is our policy on creating typiescript types for OpenMRS objects? I see in the module we create our own "OpenmrsEncounter" and "OpenmrsObs" types, for example. Are we looking to move toward a model where the Core defines types for key OpenMRS types that we can reuse, or are we just letting each ESM create their own at this point. Would be great if we had a standard library, but I realize there's value in just having each ESM create their own quick-and-dirty types for their use cases.

'custom:(uuid,display,conceptMappings:(conceptReferenceTerm:(conceptSource:(name),code)))';

export function useConcepts(references: Array<string>) {
// TODO: handle paging (ie when number of concepts greater than default limit per page)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any thoughts on this? Is there any existing pattern/code for paging through the results if necessary?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the question about existing patterns or code for paging through results, the forms-engine library does not currently define any standard patterns for pagination. However, I have a related question: Should we consider establishing a limit for the number of questions and sections allowed per page? This could help establish a standard for determining the page size.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be inclined to have a recommended max per page, not a strict max. Also, the way the code I added works, I believe it fetches all the concepts across all pages at the start, so I'm unsure if that would help in this case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking @samuelmale we can leave this as-is for now and I can create a future ticket about figuring out how to handle forms with lots of concepts?

Copy link
Member

@samuelmale samuelmale left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this library currently in active use, or is it still a work-in-progress?

The library is actively in use, and there has been a production rollout reported by ICAP last year.

I haven't looked into how the engine renders previously entered forms/encounters

When it comes to rendering previously entered forms or encounters, the data binding is handled by the useInitialValues() hook. However, the actual value mapping is determined by the configured value adapter handler (See here). To elude from breaking things, you want to look at the ObsSubmissionHandler#getInitialValue which maps obs to fields primarily by the formFieldPath (see FormRecordable.java) and then fallsback to the concept based mapping which is currently assumed to be a UUID.

What is our policy on creating typescript types for OpenMRS objects?

It is preferable to reference typings from ESM Core if they already exist.

const flattenedFieldsTemp = [];
const conceptReferencesTemp = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the high probability of duplicated entries, do we want to use an instance of a Set instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, agreed!

*/
export function findConceptByReference(reference: string, concepts) {
// we only currently support uuids and reference term pairs in the format "SOURCE:TERM", so if no ":" return null
if (!reference.includes(':')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this function should expect both reference by uuid and concept mappings:

  1. The logic that loads concepts doesn't filter out uuid type references.
  2. Additionally, we would like to load display strings that can serve as labels for questions lacking a label, where the concept reference is represented by a uuid.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, good point. When I originally wrote this method it was just about replacing mappings with uuids, but, yes, we should also match on uuid now so that we can load display strings in the uuid case as well, i will fix.

'custom:(uuid,display,conceptMappings:(conceptReferenceTerm:(conceptSource:(name),code)))';

export function useConcepts(references: Array<string>) {
// TODO: handle paging (ie when number of concepts greater than default limit per page)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the question about existing patterns or code for paging through results, the forms-engine library does not currently define any standard patterns for pagination. However, I have a related question: Should we consider establishing a limit for the number of questions and sections allowed per page? This could help establish a standard for determining the page size.

Comment on lines +237 to +249
const matchingConcept = findConceptByReference(field.questionOptions.concept, concepts);
field.questionOptions.concept = matchingConcept ? matchingConcept.uuid : field.questionOptions.concept;
field.label = field.label ? field.label : matchingConcept?.display;
if (field.questionOptions.answers) {
field.questionOptions.answers = field.questionOptions.answers.map(answer => {
const matchingAnswerConcept = findConceptByReference(answer.concept, concepts);
return {
...answer,
concept: matchingAnswerConcept ? matchingAnswerConcept.uuid : answer.concept,
label: answer.label ? answer.label : matchingAnswerConcept?.display,
};
});
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we link the matching concept to the field object for future references?

Background: We have this rarely used feature where we annotate the field with the concept name represented through a tooltip. Currently, we handle the concept fetch at the question level (see link). One important point to note is that this feature is only available in the "view" mode, this may not be the most ideal approach but it's something we intend to keep supporting.

Screenshot 2023-05-29 at 14 07 47

@mogoodrich Now that we are fetching concepts upfront, we don't need to do an extra fetch at the question level.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, thanks @samuelmale for pointing that fetch out, I hadn't noticed it. I can see if I we can replace it with the new fetch.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, sorry @samuelmale I just read this quickly the first time, but digging deeper I understand what you mean and I 100% agree that it makes sense to link the whole concept to the field. What do you think the best naming convention would be? We currently already have a "concept" on the field, but that is currently a string. We could swap out the concept string with a concept object when/if a match is found, but we'd need to go through and make sure that in all places where we reference "concept" we handle both possibilities... basically, any place where it expects "concept" to be a string we should use concept.uuid if concept is an object.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm terrible at naming things but I was thinking introducing a meta (for the lack of a better name) property at the question root level. This can be used to hold a map of arbitrary objects associated to a field.

I don't object using the existing field.questionOptions.concept property but I think we need a meta-like property to keep things in one place.

Copy link
Member Author

@mogoodrich mogoodrich May 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I liked the idea of meta a lot, but when I started to think about designing it out, I had doubts. Are you thinking adding something like this:

export interface OHRIFormQuestionOptions {
  **meta?: object;**
  extensionId?: string;
  extensionSlotName?: string;
  rendering: RenderType;
  concept?: string;
  /**
   * max and min are used to validate number field values
   */
  max?: string;
  min?: string;
  /**
   * maxLength and maxLength are used to validate text field length
   */
  maxLength?: string;
  minLength?: string;
  showDate?: string;
  answers?: Array<Record<any, any>>;
  weeksList?: string;
  locationTag?: string;
  rows?: number;
  toggleOptions?: { labelTrue: string; labelFalse: string };
  repeatOptions?: { addText?: string; limit?: string; limitExpression?: string };
  defaultValue?: any;
  calculate?: {
    calculateExpression: string;
  };
  isDateTime?: { labelTrue: boolean; labelFalse: boolean };
  usePreviousValueDisabled?: boolean;
}

When I started to map this out, the I realized that aren't a lot of the existing fields stuff we'd consider "metadata" so it might get confusing as to what lives in "meta" and what doesn't? Also, if we store all the concepts for the questions and answers within this "meta" section, we still need to figure out a way to map the answer concepts to the specific answers they go with?

Which led me to think... maybe worth starting a broader discussion about this? Is there a place where we are having Form Engine design discussions? Either Talk, or we could look into the RPC model they were using for other O3 design discussion?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, if we store all the concepts for the questions and answers within this "meta" section, we still need to figure out a way to map the answer concepts to the specific answers they go with?

I didn't think of a use case for linking the concept answers; if you asked me, I would say it's less likely to be desired. That being said, mapping ceases to be an issue.

I see in the proposed typings above you included meta to the question options, I was inclined to have it at the root level instead:

export interface OHRIFormField {
  label: string;
  type: string;
  questionOptions: OHRIFormQuestionOptions;
  id: string;
  // other props...
  meta: Record<string, any>;
}

PS: I'm honestly not so dogmatic about how we should approach this. I will let others share their thoughts; I just don't want to drag this.. cc: @ibacher, @denniskigen etc..

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Samuel... yes, let's see what Ian and Dennis think... that being said, if it becomes a dragged out discussion, I don't think blocks this PR? We could merge this in and handle adding "meta" as a future ticket?

@mogoodrich
Copy link
Member Author

When it comes to rendering previously entered forms or encounters, the data binding is handled by the useInitialValues() hook. However, the actual value mapping is determined by the configured value adapter handler (See here). To elude from breaking things, you want to look at the ObsSubmissionHandler#getInitialValue which maps obs to fields primarily by the formFieldPath (see FormRecordable.java) and then fallsback to the concept based mapping which is currently assumed to be a UUID.

Oh, great, thanks @samuelmale , I will check that out... I was hoping we were using formFieldPath, great to hear that we are.

@mogoodrich
Copy link
Member Author

Thanks @samuelmale ... this is all very helpful in helping me move forward... I will continue working on this this week and let you know when I have a new PR ready.

@mogoodrich
Copy link
Member Author

When it comes to rendering previously entered forms or encounters, the data binding is handled by the useInitialValues() hook. However, the actual value mapping is determined by the configured value adapter handler (See here). To elude from breaking things, you want to look at the ObsSubmissionHandler#getInitialValue which maps obs to fields primarily by the formFieldPath (see FormRecordable.java) and then fallsback to the concept based mapping which is currently assumed to be a UUID.

Great. taking a look into this closer @samuelmale I think that this should work fine... but Is there any way in the current UI to open a form in "edit" mode so I can test further? I can't figure it out for some reason.

(feat) O3-2129: Form Engine: Support Default Labels form obs questions and answers

/**
* Given a reference to a concept (either the uuid, or the source and reference term, ie "CIEL:1234") and a set of concepts, return matching concept, if any
*
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to support fetching concept by uuid

@mogoodrich mogoodrich marked this pull request as ready for review May 30, 2023 21:09
@mogoodrich
Copy link
Member Author

Okay, I added some tests and addressed a couple of the PR suggestions... still a couple outstanding questions to resolve, but I think this is close to being read y to merge in @samuelmale , let me know your thoughts.

@mogoodrich mogoodrich requested a review from mseaton May 30, 2023 21:15
@samuelmale
Copy link
Member

samuelmale commented May 31, 2023

When it comes to rendering previously entered forms or encounters, the data binding is handled by the useInitialValues() hook. However, the actual value mapping is determined by the configured value adapter handler (See here). To elude from breaking things, you want to look at the ObsSubmissionHandler#getInitialValue which maps obs to fields primarily by the formFieldPath (see FormRecordable.java) and then fallsback to the concept based mapping which is currently assumed to be a UUID.

Great. taking a look into this closer @samuelmale I think that this should work fine... but Is there any way in the current UI to open a form in "edit" mode so I can test further? I can't figure it out for some reason.

@mogoodrich Feel free to visit our demo site at, direct link to a patient's DB: https://ohri-demo.globalhealthapp.net/openmrs/spa/patient/2216e3ec-1984-4db0-8f03-4db848a86ab4/chart/Covid%20Assessments

"View" or "Edit" an encounter through "Actions"

Logins: admin, Admin123

@mogoodrich
Copy link
Member Author

So I was able to do some testing entering and editing forms this evening and things seem to be working well.

@samuelmale what do you think about merging this in and I can ticket the outstanding tasks (linking the full concept to the field, and handling the case where the number of concepts on a form exceeds the page limit set for the REST endpoint) just to keep this moving along?

Are there any E2E tests I should be aware of to make sure I haven't broken anything? :) Thanks!

Copy link
Member

@samuelmale samuelmale left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks @mogoodrich!

@samuelmale
Copy link
Member

Are there any E2E tests I should be aware of to make sure I haven't broken anything? :) Thanks!

We currently don't support any E2E workflow. However, earlier this month I got a chance to meet @jayasanka-sack in Addis and we briefly hinted at the need of setting up an E2E pipeline for the forms engine.

@samuelmale samuelmale merged commit 58deafa into main Jun 2, 2023
@samuelmale samuelmale deleted the O3-1911 branch June 2, 2023 13:51
@mogoodrich
Copy link
Member Author

Thanks @samuelmale !

I ticketed the two issues we discussed above:

https://issues.openmrs.org/browse/O3-2167
https://issues.openmrs.org/browse/O3-2168

I also still need to update the documentation, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants