feat(#474): generic templates and dynamic placeholder vars#808
feat(#474): generic templates and dynamic placeholder vars#808ChinHairSaintClair wants to merge 5 commits into
Conversation
9c9fcf2 to
2ecae9f
Compare
jkuester
left a comment
There was a problem hiding this comment.
Okay, first of all, sorry for the delayed review! 😓 Things have been crazy busy.
I finally got several hours this afternoon to sit down and work though this functionality thoroughly. I think before getting into the details of the code review, I want to start with a few high-level thoughts we can discuss:
Multiple templates
Your approach to switching from the place-types.json to contact-types.json makes sense and it nice and incremental. One thing I felt was annoying about the PLACE_TYPE templates, though, was this mixing of template form files, real form files, and generated form files all in the forms/contact directory (and then the place-types.json was thrown in the mix just to make things more fun...). If you knew exactly how everything worked it could make sense, but for someone not familiar with the templating, it is really confusing to grok.
One alternative structuring I have been considering is to have a forms/contact/templates directory. That would at least move the template files out of the main forms/contact directory and create a clearer delineation between which are the actual contact form and which are the templates.
Also, instead of having the contact-types.json file at all, in the forms/contact/templates directory we could have .properties.json files for each of the template forms with a template_contact_types property (or something like that) which would define which contact forms should be generated from that template. So, in forms/contact/templates you could have a patient-create.xlsx file and a patient-create.properites.json file that contains the template_contact_types of household_head and household_member. Then you have a user-create.xlsx file and a user-create.properites.json file that contains the template_contact_types of chw and chw_supervisor. The end result of convert-contact-forms should be that in forms/contact you have the files:
household_head-create.xlsxhousehold_head-create.xmlhousehold_member-create.xlsxhousehold_member-create.xmlchw-create.xlsxchw-create.xmlchw_supervisor-create.xlsxchw_supervisor-create.xml
You also get full control over which of the create/edit forms you actually create. Not saying we have to go this way if you don't think it is a good idea, but I am just looking for ways to make this work in the least surprising way (while still being feasible to maintain).
One possible challenge this creates is that it makes it even more work for cht-conf to understand which of the forms are generated from templates (and so need to have CONTACT_TYPE and CONTACT_NAME replaced in the xml). I think we could maybe simplify things a lot here by just also generating (or updating if it already exists) a .properties.json for each of the generated forms where we can make sure the CONTACT_TYPE and CONTACT_NAME values are set in the placeholder_vars object. Then, when we go to replace the form placeholders, we do not need any custom logic for generated forms.
One side-point that I think we should do either way is that IMHO we should change the logic around replacing existing copies of the generated xlsx files. With the place-types.json, when we generate an xlsx file from the template, it is not saved if a file with the same name already exists on the disk. (I assume the original intent was that someone might have edited the existing xlsx and we didn't want to blow that away when regenerating from the template. 🤷) Regardless, that behavior seems surprising and confusing to me. When converting from xlsx > xml we always blow away any existing xml content. Why not do the same for the xlsx? Also, it just makes no sense that I can run the convert with the template and generate the xlsx files. Then I update the template and run the convert again, but I do not get new versions of the xlsx files... (And if you really want to customize/keep the generated xlsx, then just remove it from the template config...)
I think we need to leave things as they are for the place-types.json config so we don't create any breaking change (until we just totally remove support for that). But for the stuff generated from the new config, I think we can replace the existing xlsx files.
Dynamic placeholder variables
I love this! I think there are a number of very interesting and powerful use-cases that this can enable both in contact and app forms! What I am not a big fan of is editing stringified xml data with Regex.... 💀 😭 I have slowly been trying to drag us away from that practice and back to a more sane world where we edit the xml Document via the standard apis and with the help of xpath queries. You can see it in action in all the handle- files I added to do some janky "massaging" of the form structure/content. In a perfect world, I would love to see us do this placeholder replacement in a similar handle- file. 🤔
The main advantage of not using regex against the whole string is that I think we could make it work without requiring the placeholders to be prefixed with __cht_var-. Instead we should be able to intelligently replace any configured value. The downside is that it is going to be more complex to do this against the xml Document. 😬
After poking around for awhile, I think the main steps for the conversion would look something like:
- Get all the nodes named with the placeholder:
//*[local-name()='PLACEHOLDER']- Rename these nodes (have to create a new node as a child of the parent, copy attributes/children, delete old node)
- Should be able to scope this to just descendants of the primary instance node.
- Get any
appearanceattributes that contain the placeholder as a "type"://@appearance[contains(., 'type-PLACEHOLDER')]- Update the
appearancevalue to replace the placeholder. - This is a specific edge case to support dynamic contact types being used with the
select-contactfunctionality.
- Update the
- Spin through all the attributes in the doc where the placeholder is being used in a path:
//@*[contains(., '/PLACEHOLDER')])`- When we rename a node, we also need to update any path references to that node (either relative or absolute).
- We can use some regex in the attribute value to make sure we only replace the placeholder when it exists in a path expression:
/\/PLACEHOLDER($|[\s*@:/\[\]])/
- Get any nodes who's value is the placeholder:
//*[text()='PLACEHOLDER']- Update the text value for these nodes
I think that logic is pretty close to complete when it comes to doing what we need. I can't say I am 100% convinced the extra complexity is worth it just to avoid the prefix, but it does also let us avoid editing xml with regex AND it keeps things simpler for all consumers going forwards since you will not have to worry about using the prefix when authoring the forms...
Should we purge template sections that are not relevant for certain types/forms?
IMHO we should not try to do this in the current iteration.
One last thing, as we add the new functionality to replace all the existing place-types.json, PLACE_TYPE, and PLACE_NAME stuff it would be convenient if we can handle that legacy functionality via the new logic. However, if that is going to add significant complexity (and lots of branching cases) my vote would be to leave the the existing place-types.json, PLACE_TYPE, and PLACE_NAME functionality as it is. We deprecate it, but don't try to refactor it. We can remove it in the next major version.
|
No stress @jkuester, I've seen some of the PR assignment requests pouring in. Can only imagine what that onslaught of reviews must be like. My turn to apologies for the late response, I was away for a bit and then had to catch up with some of our instance's fixes. Template content separation (sub dir):I agree on having a sub directory contain all the new template related forms config. The separation makes sense and it'll also make sure that we don't accidentally process templates as normal forms (ignore everything in
|
|
@jkuester Tweaked the PR based on the discussions above:
However, the PR still uses regex based variable replacement and the The prefix may also help avoid ambiguity around what will actually be treated as a dynamic placeholder inside the form. I have also left the |
Description:
This feature introduces the support for:
Multiple templates
In addition to
PLACEforms, users can now define multiplePERSONforms templates. As outlined in the original issue, this enables handling situations where multiple groups of contact forms share a similar structure:Conditional edit form creation
The
CONTACT_TYPES.jsonconfig supports an optionaltemplateEditproperty. If this property is omitted, no edit form will be created. This could reduce form maintenance overhead, where separate create/edit forms are unnecessary. In such cases, consolidating logic into a single form could help avoid drift and missed updates during ongoing maintenance.Dynamic placeholder variables
Building on the existing
PLACE_TYPEandPLACE_NAMEplaceholders, users can now define additional custom variables for each form. To avoid unintended replacements, all custom placeholders must use the__cht_var-prefix.For example, in a template it could be used to keep a spot for:
Questions:
Should we prefix template file names with a keyword?
If a
contact-types.jsonfile is not provided, the system cannot distinguish templates from standard form files. As a result, all files are processed as regular forms, which IIRC differs from the original behaviour. One possible solution is to introduce a file name prefix, for example:CONTACT-<template_name>.xlsxShould template config filenames be case- and format-insensitive? E.g:
Aligning this with the template
xlsxnaming convention (PLACE_TYPE-create.xlsx) could improve consistency.Is there a need to support templates for app forms?
The feature should be able to support that. However, the content in these forms tend to differ considerably, so it may not provide much value. That said, I'm happy to add it support if there's a use case I might have missed.
Should we purge template sections that are not relevant for certain types/forms?
For example, consider a project where
PLACESlargely share the same form structure except for one level. The additional fields for that level can be included as conditional section(s) in the template. While these sections (or fields) can be hidden at runtime based on type, their content will still exist in each copied form and must be loaded and evaluated every time the form is opened. We could instead purge these unnecessary sections/fields on template -> form creation.Issue: 804
Forum discussion:
https://forum.communityhealthtoolkit.org/t/place-type-behaviour-clarification/5514
Code review items
License
The software is provided under AGPL-3.0. Contributions to this project are accepted under the same license.