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

HTMX CSQL Fixture UI #35372

Merged
merged 9 commits into from
Feb 19, 2025
Merged

HTMX CSQL Fixture UI #35372

merged 9 commits into from
Feb 19, 2025

Conversation

esoergel
Copy link
Contributor

@esoergel esoergel commented Nov 14, 2024

Product Description

Technical Summary

Some changes to the CSQL fixture UI to make it use HTMX in a more idiomatic way. I did this a couple months ago and then kinda dropped it. I have a documentation ticket for the feature this sprint, so I'm taking the opportunity to wrap this up too.

https://dimagi.atlassian.net/browse/USH-4962

Feature Flag

CSQL_FIXTURE

Safety Assurance

FF used only by one project

Safety story

Automated test coverage

QA Plan

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 mentioned this pull request Nov 14, 2024
4 tasks
@AddisonDunn
Copy link
Contributor

Looks like a great approach, very smooth and simple.

Comment on lines 4 to 9
<form hx-post="{{ request.path_info }}"
hq-hx-action="save_expression"
hx-swap="outerHTML"
x-data="{ isUnsaved: false }"
@change="isUnsaved = true;"
@submit="isUnsaved = false;">
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
<form hx-post="{{ request.path_info }}"
hq-hx-action="save_expression"
hx-swap="outerHTML"
x-data="{ isUnsaved: false }"
@change="isUnsaved = true;"
@submit="isUnsaved = false;">
<form
hx-post="{{ request.path_info }}"
hq-hx-action="save_expression"
hx-swap="outerHTML"
x-data="{ isUnsaved: false }"
@change="isUnsaved = true;"
@submit="isUnsaved = false;"
>

{{ form.csql }}
</div>
<div class="col-2">
<button type="submit" class="btn btn-outline-primary" x-bind:disabled="!isUnsaved">
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
<button type="submit" class="btn btn-outline-primary" x-bind:disabled="!isUnsaved">
<button
class="btn btn-outline-primary" type="submit"
:disabled="!isUnsaved"
>
  1. I like to always have class be the first attribute
  2. if there is a HTMX or alpine attribute (:bind in this case), we want it on a second line
  3. I prefer shorthand alpine bindings, but could be convinced otherwise?

Comment on lines 24 to 28
<button class="btn btn-outline-danger"
hx-post="{{ request.path_info }}"
hq-hx-action="delete_expression"
hx-swap="delete"
hx-target="closest form">
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
<button class="btn btn-outline-danger"
hx-post="{{ request.path_info }}"
hq-hx-action="delete_expression"
hx-swap="delete"
hx-target="closest form">
<button
class="btn btn-outline-danger"
hx-post="{{ request.path_info }}"
hq-hx-action="delete_expression"
hx-swap="delete"
hx-target="closest form"
>

Comment on lines 25 to 30
<button type="button"
class="btn btn-default"
hx-post="{{ request.path_info }}"
hq-hx-action="new_expression"
hx-target="#csql-fixture-rows"
hx-swap="beforeend">
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
<button type="button"
class="btn btn-default"
hx-post="{{ request.path_info }}"
hq-hx-action="new_expression"
hx-target="#csql-fixture-rows"
hx-swap="beforeend">
<button
class="btn btn-default" type="button"
hx-post="{{ request.path_info }}"
hq-hx-action="new_expression"
hx-target="#csql-fixture-rows"
hx-swap="beforeend"
>

Comment on lines +21 to +22
{% for form in csql_fixture_forms %}
{{ form }}
{% endfor %}
Copy link
Member

Choose a reason for hiding this comment

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

yes, this was exactly the approach i was thinking when reviewing the page with @AddisonDunn. Less front end logic being front-loaded from the backend into Alpine, and more on the python side.

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 was super pleased to see how well this plays with Django templates! I'm still not totally sold on the second commit here though - I do think I liked it better before adding Django forms, but if I find the time I might try a compromise - like maybe I make a partial template with the <form> tags and the buttons, but have the fields themselves rendered by the Django form (at present, I set a custom template on the form, so it contains everything, which sounds nice in principle, but means I lose error handling in the template)

@esoergel
Copy link
Contributor Author

esoergel commented Feb 6, 2025

Apologies to the reviewers for neglecting this. Should be ready to go now

Copy link
Contributor

@AddisonDunn AddisonDunn left a comment

Choose a reason for hiding this comment

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

It's too bad you had to delete all my beautiful code, but I understand.

{% endblock %}

{% block modals %}
{% include "hqwebapp/htmx/error_modal.html" %}
Copy link
Contributor

Choose a reason for hiding this comment

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

How is this triggered?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The HTMX framework pops it up on an error response automatically

self.domain = domain
self.fields['name'].widget.attrs = {
'placeholder': "name",
'class': "form-control",
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you want to enforce a character limit here?

Copy link
Contributor Author

@esoergel esoergel Feb 18, 2025

Choose a reason for hiding this comment

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

Good call, will do, unless Django did that for me already. To be investigated

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just pushed a fix. Forgot that that was already in there in the default widget attrs, but I decided to override them completely instead of using .update() because I didn't like the defaults for the textarea

Copy link
Contributor

Choose a reason for hiding this comment

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

This one's not a textarea though right?

Copy link
Contributor

Choose a reason for hiding this comment

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

Or you mean you decided to override both because you didn't like the update method for one of them, the one that is a textarea

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 that's right, the latter. Felt weird to keep the defaults for one and not the other, so I figured being explicit about both was best. I'm open to reconsidering

name=instance.name,
csql=instance.csql
)
if not instance.deleted:
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the purpose of this? When would an expression be saved after being soft-deleted?

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 think this was causing an error during tests - I believe this happens during the soft delete, ie set the delete flag, then call save, then that triggers this handler

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oh that's right, it was creating an extra log entry

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I see

if form.is_valid():
form.save()
return HttpResponse(form.render())
raise AssertionError("The user shouldn't be able to submit an invalid form")
Copy link
Contributor

Choose a reason for hiding this comment

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

It doesn't seem like there are really many restrictions, right? Name or CSQL field could be empty.

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 - this form doesn't have much validation. I just added this because if you did throw an error, it wouldn't be displayed well in the UI. I wanted to come back and fix that, but ultimately gave up, particularly since it doesn't actually apply here.

@esoergel esoergel merged commit 8c45d96 into master Feb 19, 2025
14 checks passed
@esoergel esoergel deleted the es/htmx-badges branch February 19, 2025 21:05
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