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

End-of-form navigation manual linking advanced mode #33950

Closed
wants to merge 14 commits into from

Conversation

esoergel
Copy link
Contributor

@esoergel esoergel commented Jan 5, 2024

Product Description

Adds an "advanced mode" feature flag for end of form navigation manual linking.

Without the feature flag:
image

With the feature flag:
image

Technical Summary

https://dimagi-dev.atlassian.net/browse/USH-3966
This PR incorporates work @MartinRiese did a bit ago to include a broader set of modules in the set of linkable targets for EoF nav.

EoF navigation involves populating the stack to navigate through the app to the desired target. Some forms and modules expect certain variables (like case IDs) to be available in the session. Automatic linking attempts to align existing session variables with those needed to navigate to the target. Where that isn't possible, the app builder must provide an xpath expression for each of these datum IDs. This manual linking is also sometimes used when automatic linking doesn't do what's desired.

However, we can't always reliably get the set of datum IDs needed to get to a target without going through the full build process, especially when child and shadow modules are involved, as the datum IDs are modified at various stages in the build process.

This PR introduces an "advanced mode" feature flag for manual linking that essentially gives up trying to predict the IDs needed, and requires the user to specify all sets of IDs and xpath expressions needed to navigate to the endpoint. This will likely be a pain to use, though build validation should catch errors here.

Depending on how this is used, we may consider an extension of this work where advanced mode is used only on a case-by-case basis, so app builders could decide whether to enable it per module, and otherwise use the existing manual linking UI (we'd need to ensure switching between the two doesn't break anything).

Feature Flag

This adds a new feature flag:
FORM_LINK_ADVANCED_MODE - USH: Form linking advanced mode

Safety Assurance

Safety story

Testing on staging. The UI itself is pretty small, it uses an existing widget for specifying key/value pairs. Everything should be sufficiently hidden behind the feature flag that I'm not too concerned about affecting projects that don't enable the FF.

Automated test coverage

none

QA Plan

none

Rollback instructions

  • This PR can be reverted after deploy with no further considerations

Labels & Review

  • Risk label is set correctly
  • The set of people pinged as reviewers is appropriate for the level of risk of the change

@esoergel esoergel force-pushed the es/riese/linking branch 2 times, most recently from 02c7214 to fcbd8ab Compare January 5, 2024 21:17
@esoergel esoergel requested a review from MartinRiese January 5, 2024 21:20
@@ -742,6 +742,18 @@ def _ensure_valid_randomness(randomness):
[NAMESPACE_DOMAIN],
)

FORM_LINK_ADVANCED_MODE = StaticToggle(
Copy link
Contributor

Choose a reason for hiding this comment

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

will this be a temporary FF? We really are pretty quick to add new ones aren't we?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't see an alternative, unfortunately (though I'm open to ideas!). This feature is generally available for some reason, even though it's pretty brittle. I was thinking about trying to integrate the advanced mode into the normal manual mode as mentioned in the description, and may do that if delivery advocates for it. However, I don't think it's a good idea to increase the set of linkable targets to GA audiences, knowing that they don't work unless you are able to figure out the "advanced mode" linking stuff.

Copy link
Contributor

Choose a reason for hiding this comment

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

That makes sense. Curious if you would see any value in have a parent FF like "experimental" to group these kinds of FF and give them a bit more context?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You mean like a separate feature flag tag? I don't know, would something in the description make sense? This seems fairly in line with other custom feature flags to me - what's the information you're thinking about conveying?

@@ -143,6 +144,7 @@ hqDefine('app_manager/js/forms/form_workflow', function () {
self.formId = ko.observable(formId);
self.autoLink = ko.observable();
self.allowManualLinking = ko.observable();
self.advancedMode = hqImport("hqwebapp/js/toggles").toggleEnabled('FORM_LINK_ADVANCED_MODE');
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe advancedLinkingMode would make it clearer what this does.

gettext("Set datums required to navigate to the selected form or menu"),
{key: gettext("Datum ID"), value: gettext("XPath Expression")} // placeholders
);
let datumsDict = {};
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this can be a const. I don't see you reassigning it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking of it as something that would be mutated (ie, not a constant), but reading over this page, I think I'm coming to agree with you.

You should understand const declarations as "create a variable whose identity remains constant", not "whose value remains constant"

Many style guides (including MDN's) recommend using const over let whenever a variable is not reassigned in its scope. This makes the intent clear that a variable's type (or value, in the case of a primitive) can never change. Others may prefer let for non-primitives that are mutated.

Javascript is frustrating sometimes.

});
self.advancedModeDatumsElement.val(datumsDict);
self.advancedModeDatumsElement.on("change", function () {
self.serializedDatums(JSON.stringify(_.map(this.val(), function (xpath, name) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Just making sure I understand this correctly. If we are not in advanced mode self.serializedDatums will be used by the formId element. But you can only be in "normal" or "advanced". So there is no conflict? Is that correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's right - serializedDatums will be controlled by one or the other. I do wish this had further separation though, like some sort of composable element that treats the variable stuff as a widget where you can swap between one or the other. Though I think if I put much more work into this UI I'd probably drop this widget entirely and work on combining the two UIs (maybe, I don't know - I don't trust the old UI, and that might be worse, for instance if the displayed case type is wrong).

Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like having a follow up story/pr for cleaning up the config ui would be reasonable

Copy link
Contributor

@gherceg gherceg left a comment

Choose a reason for hiding this comment

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

@nospame and I reviewed this. It looks good so far and left a couple of clarifying comments.

Comment on lines +999 to +1000
if '.' in form_id:
module_id, form_id = form_id.split('.')
Copy link
Contributor

Choose a reason for hiding this comment

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

Naive question, but is it a fair assumption that the form_id will always be in the format "module.form" (or "module") and not "parent.child.form_id"?

Copy link
Contributor Author

@esoergel esoergel Jan 10, 2024

Choose a reason for hiding this comment

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

that's correct - it's set here:

'unique_id': f'{candidate_module.unique_id}.{candidate_form.unique_id}',

(and a little above that for modules, which don't contain the .)

@@ -981,7 +981,7 @@ def _module_hierarchy(module):
def get_form_datums(request, domain, app_id):
form_id = request.GET.get('form_id')
try:
datums = _get_form_datums(domain, app_id, form_id)
datums = _get_form_link_datums(domain, app_id, form_id)
Copy link
Contributor

Choose a reason for hiding this comment

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

If get_form_datums also just returns "form_link_datums", does it make sense to rename that as well? This is coming from a place of not understanding the details of _get_form_link_datums and why we only return datums where requires_selection is true as someone reading through this code for the first time. Maybe it could be useful to document that somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah I think it makes sense to rename the view (and URL and what not) too. Or at least keep them consistent. I'll do that.

Though I think I might actually walk back the changes to this function since it doesn't apply to advanced mode datums and modules aren't linkable aside from in that mode that. This work has seen a decent bit of churn as we try to get something working (it's still being tested on staging right now, actually, though I think the current issues are on the formplayer side of things). An earlier version intended to extend the manual linking to modules, but it just didn't work in practice (we know that the existing version of this feature has edge cases that fail).

why we only return datums where requires_selection is true

This is documented a bit here:

class FormLink(DocumentSchema):
"""
FormLinks are advanced end of form navigation configuration, used when a module's
post_form_workflow is WORKFLOW_FORM.
They allow the user to specify one or more XPath expressions, each with either
a module id or form id. The user will be sent to the first module/form whose
expression evaluates to true. If none of the conditions is met, the workflow specified
in the module's post_form_workflow_fallback is executed.
xpath: XPath condition that must be true in order to execute link
form_id: ID of next form to open, mutually exclusive with module_unique_id
form_module_id: ID of the form's module (this is used for shadow modules)
module_unique_id: ID of next module to open, mutually exclusive with form_id
datums: Any user-provided datums, necessary when HQ can't figure them out automatically

And in a broader context on readthedocs.

The gist of it is that to navigate to a form or menu, you have to provide datums that "require selection", like choosing a case from a case list. Those that don't require selection don't need to be provided manually, they can be inferred.

@esoergel
Copy link
Contributor Author

Closing in favor of #34037, which squashes and reorganizes the commits so they're easier to review, and removes some unneeded changes from this PR.

@esoergel esoergel closed this Jan 26, 2024
@esoergel esoergel deleted the es/riese/linking branch January 26, 2024 23:00
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.

3 participants