Dynamic tables with htmx

- Search is triggered with user inputs
- Page changes are triggered with clicks
This commit is contained in:
Éloi Rivard 2023-03-09 17:41:26 +01:00
parent 2d0c58c3e3
commit cf9b5c11a3
14 changed files with 122 additions and 69 deletions

View file

@ -29,6 +29,7 @@ from .apputils import obj_to_b64
from .apputils import profile_hash
from .flaskutils import current_user
from .flaskutils import permissions_needed
from .flaskutils import render_htmx_template
from .flaskutils import smtp_needed
from .flaskutils import user_needed
from .forms import FirstLoginForm
@ -187,7 +188,7 @@ def users(user):
if request.form and not table_form.validate():
abort(404)
return render_template(
return render_htmx_template(
"users.html",
menuitem="users",
table_form=table_form,

View file

@ -7,6 +7,7 @@ from canaille.models import User
from flask import abort
from flask import current_app
from flask import render_template
from flask import request
from flask import session
from flask_babel import gettext as _
@ -84,3 +85,12 @@ def set_parameter_in_url_query(url, **kwargs):
parameters = {**parameters, **kwargs}
split[3] = "&".join(f"{key}={value}" for key, value in parameters.items())
return urlunsplit(split)
def render_htmx_template(template, htmx_template=None, **kwargs):
template = (
(htmx_template or f"partial/{template}")
if request.headers.get("HX-Request")
else template
)
return render_template(template, **kwargs)

View file

@ -8,6 +8,7 @@ from flask_babel import gettext as _
from flask_themer import render_template
from .flaskutils import permissions_needed
from .flaskutils import render_htmx_template
from .forms import CreateGroupForm
from .forms import EditGroupForm
from .forms import TableForm
@ -24,7 +25,7 @@ def groups(user):
if request.form and request.form.get("page") and not table_form.validate():
abort(404)
return render_template("groups.html", menuitem="groups", table_form=table_form)
return render_htmx_template("groups.html", menuitem="groups", table_form=table_form)
@bp.route("/add", methods=("GET", "POST"))
@ -96,8 +97,9 @@ def edit_group(group):
else:
flash(_("Group edition failed."), "error")
return render_template(
return render_htmx_template(
"group.html",
"partial/users.html",
form=form,
edited_group=group,
table_form=table_form,

View file

@ -1,4 +1,5 @@
from canaille.flaskutils import permissions_needed
from canaille.flaskutils import render_htmx_template
from canaille.forms import TableForm
from canaille.oidc.models import AuthorizationCode
from flask import abort
@ -17,7 +18,7 @@ def index(user):
if request.form and request.form.get("page") and not table_form.validate():
abort(404)
return render_template(
return render_htmx_template(
"oidc/admin/authorization_list.html",
menuitem="admin",
table_form=table_form,

View file

@ -1,6 +1,7 @@
import datetime
from canaille.flaskutils import permissions_needed
from canaille.flaskutils import render_htmx_template
from canaille.forms import TableForm
from canaille.oidc.forms import ClientAdd
from canaille.oidc.models import Client
@ -25,7 +26,7 @@ def index(user):
if request.form and request.form.get("page") and not table_form.validate():
abort(404)
return render_template(
return render_htmx_template(
"oidc/admin/client_list.html", menuitem="admin", table_form=table_form
)

View file

@ -1,6 +1,7 @@
import datetime
from canaille.flaskutils import permissions_needed
from canaille.flaskutils import render_htmx_template
from canaille.forms import TableForm
from canaille.models import User
from canaille.oidc.models import Client
@ -24,7 +25,7 @@ def index(user):
if request.form and request.form.get("page") and not table_form.validate():
abort(404)
return render_template(
return render_htmx_template(
"oidc/admin/token_list.html", menuitem="admin", table_form=table_form
)

View file

@ -1,5 +1,6 @@
{% extends theme('base.html') %}
{% import 'macro/fomanticui.html' as sui %}
{% import "macro/table.html" as table %}
{% block script %}
<script src="/static/js/confirm.js"></script>
@ -87,6 +88,9 @@
</div>
</h2>
<div class="ui attached segment">
{{ table.search(table_form, "table.users") }}
</div>
{% include "partial/users.html" %}
</div>
{% endif %}

View file

@ -13,7 +13,7 @@
</div>
</h2>
<div class="ui attached segment">
{{ table.search(table_form) }}
{{ table.search(table_form, "table.groups") }}
</div>
{% include "partial/groups.html" %}
</div>

View file

@ -1,70 +1,100 @@
{% macro search(form) %}
{% macro search(form, target) %}
<form id="search" action="{{ url_for(request.url_rule.endpoint, **request.view_args) }}" method="POST" class="ui form">
{{ form.hidden_tag() if form.hidden_tag }}
<input type="hidden" name="page" value="{{ form.page.data }}">
<div class="ui fluid action input">
<input type="search" placeholder="{{ _("Search") }}" name="{{ form.query.name }}" value="{{ form.query.data }}">
<button type="submit" class="ui icon button" title="{{ _("Search") }}">
<div class="ui fluid action input icon">
<input
type="search"
placeholder="{{ _("Search…") }}"
name="{{ form.query.name }}"
value="{{ form.query.data }}"
hx-post="{{ url_for(request.url_rule.endpoint, **request.view_args) }}"
hx-trigger="keyup changed delay:500ms, search"
hx-target="{{ target }}"
hx-swap="outerHTML"
hx-indicator=".search-button"
/>
<button type="submit" class="ui icon button search-button" title="{{ _("Search") }}">
<i class="search icon"></i>
</button>
</div>
</form>
{% endmacro %}
{% macro pagination(form) %}
<form id="pagination" action="{{ url_for(request.url_rule.endpoint, **request.view_args) }}" method="POST" class="ui form">
{# At the moment we need to build one form per button
# https://github.com/bigskysoftware/htmx/issues/1120
# when this is fixed we will be able to set the page
# value directly in the submit button and get rid
# of the radius reset
#}
{% macro buttonform(form, page) %}
<form id="pagination" action="{{ url_for(request.url_rule.endpoint, **request.view_args) }}" method="POST">
{{ form.hidden_tag() if form.hidden_tag }}
<input type="hidden" name="page" value="{{ page }}">
<input type="hidden" name="query" value="{{ form.query.data }}">
<div class="ui right floated stackable buttons">
<span class="icon disabled ui button">
{% trans %}Page{% endtrans %}
</span>
{% if form.page.data > 1 %}
<button name="page" type="submit" class="icon ui button" value="{{ form.page.data - 1 }}">
<i class="left chevron icon"></i>
</button>
{% else %}
<span class="icon disabled ui button">
<i class="left chevron icon"></i>
</span>
{% endif %}
{% if form.page.data > 1 %}
<button name="page" type="submit" class="ui button" value="1">
1
</button>
{% endif %}
{% if form.page.data > 2 %}
<span class="disabled ui button">
</span>
{% endif %}
<span class="ui button active">
{{ form.page.data }}
</span>
{% if form.page.data < form.page_max - 1 %}
<span class="disabled ui button">
</span>
{% endif %}
{% if form.page.data < form.page_max %}
<button name="page" type="submit" class="ui button" value="{{ form.page_max }}">
{{ form.page_max }}
</button>
{% endif %}
{% if form.page.data < form.page_max %}
<button name="page" type="submit" class="icon ui button" value="{{ form.page.data + 1 }}">
<i class="right chevron icon"></i>
</button>
{% else %}
<span class="icon disabled ui button">
<i class="right chevron icon"></i>
</span>
{% endif %}
</div>
<div class="ui left floated">
<span class="disabled ui button">
{{ _("%(nb_items)s items", nb_items=form.nb_items) }}
</span>
</div>
<button
type="submit"
style="border-radius: 0"
class="icon ui button"
hx-post="{{ url_for(request.url_rule.endpoint, **request.view_args) }}"
hx-target="closest table"
hx-swap="outerHTML"
>
{{ caller() }}
</button>
</form>
{% endmacro %}
{% macro pagination(form) %}
<div class="ui right floated buttons">
<span class="icon disabled ui button" style="border-radius: 0">
{% trans %}Page{% endtrans %}
</span>
{% if form.page.data > 1 %}
{% call buttonform(form, form.page.data - 1) %}
<i class="left chevron icon"></i>
{% endcall %}
{% else %}
<span class="icon disabled ui button" style="border-radius: 0">
<i class="left chevron icon"></i>
</span>
{% endif %}
{% if form.page.data > 1 %}
{% call buttonform(form, 1) %}
1
{% endcall %}
{% endif %}
{% if form.page.data > 2 %}
<span class="disabled ui button" style="border-radius: 0">
</span>
{% endif %}
<span class="ui button active">
{{ form.page.data }}
</span>
{% if form.page.data < form.page_max - 1 %}
<span class="disabled ui button" style="border-radius: 0">
</span>
{% endif %}
{% if form.page.data < form.page_max %}
{% call buttonform(form, form.page_max) %}
{{ form.page_max }}
{% endcall %}
{% endif %}
{% if form.page.data < form.page_max %}
{% call buttonform(form, form.page.data + 1) %}
<i class="right chevron icon"></i>
{% endcall %}
{% else %}
<span class="icon disabled ui button" style="border-radius: 0">
<i class="right chevron icon"></i>
</span>
{% endif %}
</div>
<div class="ui left floated">
<span class="disabled ui button">
{{ _("%(nb_items)s items", nb_items=form.nb_items) }}
</span>
</div>
{% endmacro %}

View file

@ -7,7 +7,7 @@
{% block content %}
<div class="ui attached segment">
{{ table.search(table_form) }}
{{ table.search(table_form, "table.codes") }}
</div>
{% include "partial/oidc/admin/authorization_list.html" %}
{% endblock %}

View file

@ -12,7 +12,7 @@
</div>
<div class="ui attached segment">
{{ table.search(table_form) }}
{{ table.search(table_form, "table.clients") }}
</div>
{% include "partial/oidc/admin/client_list.html" %}
{% endblock %}

View file

@ -7,7 +7,7 @@
{% block content %}
<div class="ui attached segment">
{{ table.search(table_form) }}
{{ table.search(table_form, "table.tokens") }}
</div>
{% include "partial/oidc/admin/token_list.html" %}
{% endblock %}

View file

@ -15,7 +15,7 @@
</div>
</div>
<div class="ui attached segment">
{{ table.search(table_form) }}
{{ table.search(table_form, "table.users") }}
</div>
{% include "partial/users.html" %}
{% endblock %}

View file

@ -105,10 +105,13 @@
<footer>
<a href="{{ url_for('account.about') }}">{{ _("About Canaille") }}</a>
</footer>
<script src="/static/htmx/htmx.min.js"></script>
<script src="/static/jquery/jquery.min.js"></script>
<script src="/static/fomanticui/semantic.min.js"></script>
<script src="/static/htmx/htmx.min.js"></script>
<script src="/static/js/base.js"></script>
<script>
htmx.config.requestClass = "loading"
</script>
{% block script %}{% endblock %}
</body>
</html>