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

[Draft--WIP] HTMX tests (data cleaning prototype) #34830

Closed
wants to merge 51 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
366d46f
add htmx to package.json
biyeun Jun 12, 2024
fd492b6
include htmx in our javascript as a core library
biyeun Jun 13, 2024
29126dc
add django tables
biyeun Jun 17, 2024
472be05
add multi-column sorting capability for django tables
biyeun Jun 18, 2024
0ccb5b9
set up base templates for django tables, with HTMX support
biyeun Jun 18, 2024
866c9e0
add styles for django tables
biyeun Jun 18, 2024
31c0837
add placeholder for htmx tests
biyeun Jun 18, 2024
b2cf4a2
add simple pagination of table example with HTMX
biyeun Jun 18, 2024
2e7eb27
add sortablejs
biyeun Jun 20, 2024
28779f9
add alpine.js
biyeun Jun 20, 2024
835a624
add alpine js decorator and include in base template
biyeun Jun 20, 2024
0b53c8c
add ability to trigger a referesh of the table with the refreshTable …
biyeun Jun 20, 2024
a7dc5d4
add loading indicator for table
biyeun Jun 20, 2024
475c135
update formatting
biyeun Jun 20, 2024
0a23cc5
split off saved pagination state to separate mixin, create SavedPagin…
biyeun Jun 20, 2024
8227c72
add models folder in prototype app
biyeun Jun 20, 2024
755de38
add scaffolding for data cleaning prototype
biyeun Jun 20, 2024
47ca62b
add forms folder
biyeun Jun 20, 2024
123a207
move alpinejs before core libraries
biyeun Jun 25, 2024
ed2aeb9
un-bundle htmx from requirejs
biyeun Jun 25, 2024
da29dcb
propagate pagination changes to parent page
biyeun Jun 25, 2024
7dfb08c
add styling for select column
biyeun Jun 25, 2024
f207d2b
saas design sprint prototype, minus data editing forms
biyeun Jun 25, 2024
2192705
quick utility to reset data
biyeun Jun 27, 2024
343d483
increase timeout for cache stores
biyeun Jun 27, 2024
520905a
add ability to delete cache store data
biyeun Jun 27, 2024
49c03f5
update permissions to prototype flag
biyeun Jun 27, 2024
77c63a5
add data editing capabilities for prototype
biyeun Jun 27, 2024
8b5089b
update decorators for pagination example
biyeun Jun 27, 2024
411746e
move data_cleaning.js out of htmx folder
biyeun Jun 27, 2024
a71e100
inline editing in table cell styling
biyeun Jun 28, 2024
c0589d6
inline editing of table cell value with htmx and alpine
biyeun Jun 28, 2024
bc914e5
thanks to hx-target being the parent table, hx-swap is not needed here
biyeun Jun 28, 2024
d0c9be0
narrow the width of selected column
biyeun Jun 28, 2024
674384b
allow partial updates for cell data changes
biyeun Jun 28, 2024
218911c
update translations
biyeun Jun 28, 2024
8d53ce6
use self.data_store
biyeun Jun 28, 2024
e730240
add support for handling multiple hx-action requests to a TemplateView
biyeun Jul 1, 2024
4dcfac8
use HTMX actions with all table hx requests and merge DataCleaningCel…
biyeun Jul 1, 2024
85c68ed
add placeholder for htmx error handling
biyeun Jul 1, 2024
84a486c
update error handling modal to get information from htmx event
biyeun Jul 2, 2024
73ef1e2
rename view for clarity
biyeun Jul 2, 2024
1e0999f
cleanup variable names and attribute order. remove unused ids
biyeun Jul 2, 2024
1257e64
remove unused event
biyeun Jul 2, 2024
2579f81
update container_id usage for forms
biyeun Jul 2, 2024
ea84211
cleanup unused ids, add translations
biyeun Jul 2, 2024
2d57239
add clarity to select all functionality
biyeun Jul 2, 2024
f7decbf
add fyi
biyeun Jul 2, 2024
3b16d1f
update apply edits button when values are edited (or cancelled)
biyeun Jul 2, 2024
58ecf25
add translations
biyeun Jul 2, 2024
9f2bbc8
disable clean data button unless records are selected
biyeun Jul 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions corehq/apps/hqwebapp/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,52 @@
from corehq.apps.hqwebapp.utils.bootstrap import set_bootstrap_version5


def use_alpinejs(view_func):
"""Use this decorator on the dispatch method of a TemplateView subclass
to use Alpine.js on a page.
Note: Eventually this will be bundled with Webpack. The es6/esm module
is incompatible with Requirejs and didn't seem fitting to add additional tooling
to support this in Requirejs since alpine.js lives primarily in HTML attributes.
Can be referenced in javascript with `window.Alpine` if necessary.

Example:
@use_alpinejs
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

Or alternatively:
@method_decorator(use_alpinejs, name='dispatch')
class MyViewClass(MyViewSubclass):
...
"""
return set_request_flag(view_func, 'use_alpinejs')


def use_htmx(view_func):
"""Use this decorator on the dispatch method of a TemplateView subclass
to use HTMX on a page.

Note: Bundling HTMX with RequireJS can be done by setting window.htmx = htmx
after importing the htmx module. However, some page loads caused HTMX to not initialize
properly on a page, resulting in components that never loaded. Importing the HTMX script
directly in <head> first thing proves to be the more performant and reliable way to get
HTMX initialize reliably and quickly.

TODO: Investigate bundling HTMX with Webpack.

Example:
@use_htmx
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

Or alternatively:
@method_decorator(use_htmx, name='dispatch')
class MyViewClass(MyViewSubclass):
...
"""
return set_request_flag(view_func, 'use_htmx')


def use_daterangepicker(view_func):
"""Use this decorator on the dispatch method of a TemplateView subclass
to enable the inclusion of the daterangepicker library at the base template
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ requirejs.config({
"ko.mapping": "hqwebapp/js/lib/knockout_plugins/knockout_mapping.ko.min",
"sentry_browser": "sentry/js/sentry.browser.7.28.0.min",
"sentry_captureconsole": "sentry/js/sentry.captureconsole.7.28.0.min",
"sortablejs": "sortablejs/Sortable.min",
"underscore": "underscore/underscore",
},
shim: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ requirejs.config({
"popper": "@popperjs/core/dist/umd/popper.min",
"sentry_browser": "sentry/js/sentry.browser.7.28.0.min",
"sentry_captureconsole": "sentry/js/sentry.captureconsole.7.28.0.min",
"sortablejs": "sortablejs/Sortable.min",
"tempusDominus": "@eonasdan/tempus-dominus/dist/js/tempus-dominus.min",
"underscore": "underscore/underscore",
"nvd3/nv.d3.latest.min": "nvd3-1.8.6/build/nv.d3.min",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,92 @@
.table-editprops-filterval {
min-width: 115px;
}

.table thead tr th.orderable {
position: relative;
padding: 0;

a {
display: block;
background-color: $blue-800;
color: $white;
padding: 0.5rem 0.5rem;
}
&:nth-child(odd) a {
background-color: $blue-700;
}

&::before,
&::after {
position: absolute;
display: block;
right: 10px;
line-height: 9px;
font-size: .8em;
color: $white;
opacity: 0.3;
}

&::before {
bottom: 50%;
content: "▲" / "";
}

&::after {
top: 50%;
content: "▼" / "";
}

&.asc::before {
opacity: 1.0;
}
&.desc::after {
opacity: 1.0;
}
}

.table thead tr th.select-header {
width: 28px;
background-color: $blue-800;

&:nth-child(odd) {
background-color: $blue-700;
}
}


.table tbody tr td .inline-edit-block {
display: block;
background-color: transparent;
transition: background-color .5s ease-in-out;
position: relative;

.inline-edit-button {
position: absolute;
right: 0;
top: calc(50% - 12px);
opacity: 0;
transition: opacity .5s ease-in-out;
}

&:hover {
background-color: $gray-200;
&:nth-child(odd) {
background-color: $gray-300;
}
}

&:hover .inline-edit-button {
display: block;
opacity: 1;
}
}

.table tbody tr.table-primary td .inline-edit-block {
&:hover {
background-color: $blue-100;
&:nth-child(odd) {
background-color: $blue-200;
}
}
}
7 changes: 7 additions & 0 deletions corehq/apps/hqwebapp/templates/hqwebapp/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@
window.USE_BOOTSTRAP5 = {{ use_bootstrap5|BOOL }};
</script>

{% if request.use_htmx %}
<script type="text/javascript" src="{% static "htmx.org/dist/htmx.min.js" %}"></script>
{% endif %}
{% if request.use_alpinejs %}
<script type="text/javascript" src="{% static 'alpinejs/dist/cdn.min.js' %}"></script>
{% endif %}

{% if not requirejs_main %}
{% javascript_libraries use_bootstrap5=use_bootstrap5 underscore=True jquery_ui=request.use_jquery_ui ko=True hq=True analytics=True %}
{% endif %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
hq: Include initial_page_data and hq.helpers.js, needed for most HQ pages.
{% endcomment %}

<script src="{% static "sortablejs/Sortable.min.js" %}"></script>

{% if underscore or analytics or hq %}
<script src="{% static 'underscore/underscore-min.js' %}"></script>
<script src="{% static 'hqwebapp/js/lib/modernizr.js' %}"></script>
Expand Down
97 changes: 97 additions & 0 deletions corehq/apps/hqwebapp/templates/hqwebapp/tables/bootstrap5.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
{% extends 'django_tables2/bootstrap5.html' %}
{% load i18n %}
{% load django_tables2 %}
{% load hq_shared_tags %}

{% block table-wrapper %}
<div class="table-container"
{% block table-container-attrs %}{% endblock %}
{% if table.css_id %} id="{{ table.css_id }}"{% endif %}>
{% block before_table %}{% endblock %}
{% block table %}
{{ block.super }}
{% endblock table %}
<div class="py-3 d-flex justify-content-between">
<div>
{% block num_entries %}
<div class="input-group">
<div class="input-group-text">
{% block num_entries.text %}
{% with start=table.page.start_index end=table.page.end_index total=table.page.paginator.count %}
{% blocktranslate %}
Showing {{ start }} to {{ end }} of {{ total }} entries
{% endblocktranslate %}
{% endwith %}
{% endblock num_entries.text %}
</div>
{% block num_entries.select %}
<select class="form-select"
{% block select-per-page-attr %}{% endblock %}>
{% for p in table.paginator.paging_options %}
<option value="{{ p }}"{% if p == table.paginator.per_page %} selected{% endif %}>
{% blocktrans %}{{ p }} per page{% endblocktrans %}
</option>
{% endfor %}
</select>
{% endblock %}
</div>
{% endblock num_entries %}
</div>
<div>
{% block pagination %}
{% if table.page and table.paginator.num_pages > 1 %}
<nav aria-label="Table navigation">
<ul class="pagination">

{% block pagination.previous %}
<li class="previous page-item{% if not table.page.has_previous %} disabled{% endif %}">
<a class="page-link"
{% if table.page.has_previous %}
{% block prev-page-link-attr %}
href="{% querystring table.prefixed_page_field=table.page.previous_page_number %}"
{% endblock %}
{% endif %}>
{% trans 'Previous' %}
</a>
</li>
{% endblock pagination.previous %}

{% if table.page.has_previous or table.page.has_next %}
{% block pagination.range %}
{% for p in table.page|table_page_range:table.paginator %}
<li class="page-item{% if table.page.number == p %} active{% endif %}">
<a class="page-link" {% if p != '...' %}href="{% querystring table.prefixed_page_field=p %}"{% endif %}>
{{ p }}
</a>
</li>
{% endfor %}
{% endblock pagination.range %}
{% endif %}

{% block pagination.next %}
<li class="next page-item{% if not table.page.has_next %} disabled{% endif %}">
<a class="page-link"
{% if table.page.has_next %}
{% block next-page-link-attr %}
href="{% querystring table.prefixed_page_field=table.page.next_page_number %}"
{% endblock %}
{% endif %}>
{% trans 'Next' %}
</a>
</li>
{% endblock pagination.next %}

</ul>
</nav>
{% endif %}
{% endblock pagination %}
</div>
</div>
</div>
{% endblock table-wrapper %}

{% block table.thead %}
{% if table.show_header %}
{% render_header %}
{% endif %}
{% endblock table.thead %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{% extends "hqwebapp/tables/bootstrap5.html" %}
{% load i18n %}
{% load django_tables2 %}
{% load hq_shared_tags %}

{% block table.thead %}
{% if table.show_header %}
{% render_header 'htmx' %}
{% endif %}
{% endblock table.thead %}

{% block table-container-attrs %}
hx-get="{{ request.path_info }}{% querystring %}"
hx-replace-url="{% querystring %}"
hx-trigger="refreshTable"
{% endblock %}

{% block before_table %}
<div class="htmx-indicator">
<i class="fa-solid fa-spinner fa-spin"></i> {% trans "Updating..." %}
</div>
{% endblock %}

{% block select-per-page-attr %}
name="{{ table.per_page_field }}"
hx-get="{{ request.path_info }}"
hx-target="{% if table.css_id %}#{{ table.css_id }}{% else %}div.table-container{% endif %}"
{% endblock %}

{% block prev-page-link-attr %}
hx-get="{{ request.path_info }}{% querystring table.prefixed_page_field=table.page.previous_page_number %}"
hx-replace-url="{% querystring table.prefixed_page_field=table.page.previous_page_number %}"
hx-trigger="click"
hx-target="{% if table.css_id %}#{{ table.css_id }}{% else %}div.table-container{% endif %}"
hx-swap="outerHTML"
{% endblock %}

{% block next-page-link-attr %}
hx-get="{{ request.path_info }}{% querystring table.prefixed_page_field=table.page.next_page_number %}"
hx-replace-url="{% querystring table.prefixed_page_field=table.page.next_page_number %}"
hx-trigger="click"
hx-target="{% if table.css_id %}#{{ table.css_id }}{% else %}div.table-container{% endif %}"
hx-swap="outerHTML"
{% endblock %}

{% block pagination.range %}
{% for p in table.page|table_page_range:table.paginator %}
<li class="page-item{% if table.page.number == p %} active{% endif %}">
<a class="page-link"
{% if p != '...' %}
hx-get="{{ request.path_info }}{% querystring table.prefixed_page_field=p %}"
hx-replace-url="{% querystring table.prefixed_page_field=p %}"
hx-trigger="click"
hx-target="{% if table.css_id %}#{{ table.css_id }}{% else %}div.table-container{% endif %}"
hx-swap="outerHTML"
{% endif %}>
{{ p }}
</a>
</li>
{% endfor %}
{% endblock pagination.range %}
37 changes: 37 additions & 0 deletions corehq/apps/hqwebapp/templates/hqwebapp/tables/header.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{% load querystring from django_tables2 %}

<thead {{ table.attrs.thead.as_html }}>
<tr>
{% for column in table.columns %}
<th {{ column.attrs.th.as_html }} scope="col">
{% if column.orderable %}
{% if column.sort_existing %}
<a {% if use_htmx_links %}class="link"
hx-get="{{ request.path_info }}{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}"
hx-trigger="click"
hx-target="{% if table.css_id %}#{{ table.css_id }}{% else %}div.table-container{% endif %}"
hx-swap="outerHTML"
{% else %}
href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}"
{% endif %}>
{{ column.header }}
</a>
{% else %}
<a {% if use_htmx_links %}class="link"
hx-get="{{ request.path_info }}{% querystring %}&{{ table.prefixed_order_by_field }}={{ column.order_by_alias.next }}"
hx-trigger="click"
hx-target="{% if table.css_id %}#{{ table.css_id }}{% else %}div.table-container{% endif %}"
hx-swap="outerHTML"
{% else %}
href="{% querystring %}&{{ table.prefixed_order_by_field }}={{ column.order_by_alias.next }}"
{% endif %}>
{{ column.header }}
</a>
{% endif %}
{% else %}
{{ column.header }}
{% endif %}
</th>
{% endfor %}
</tr>
</thead>
Loading
Loading