forked from Github-Mirrors/canaille
Dynamic tables with htmx
- Search is triggered with user inputs - Page changes are triggered with clicks
This commit is contained in:
parent
2d0c58c3e3
commit
cf9b5c11a3
14 changed files with 122 additions and 69 deletions
|
@ -29,6 +29,7 @@ from .apputils import obj_to_b64
|
||||||
from .apputils import profile_hash
|
from .apputils import profile_hash
|
||||||
from .flaskutils import current_user
|
from .flaskutils import current_user
|
||||||
from .flaskutils import permissions_needed
|
from .flaskutils import permissions_needed
|
||||||
|
from .flaskutils import render_htmx_template
|
||||||
from .flaskutils import smtp_needed
|
from .flaskutils import smtp_needed
|
||||||
from .flaskutils import user_needed
|
from .flaskutils import user_needed
|
||||||
from .forms import FirstLoginForm
|
from .forms import FirstLoginForm
|
||||||
|
@ -187,7 +188,7 @@ def users(user):
|
||||||
if request.form and not table_form.validate():
|
if request.form and not table_form.validate():
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
return render_template(
|
return render_htmx_template(
|
||||||
"users.html",
|
"users.html",
|
||||||
menuitem="users",
|
menuitem="users",
|
||||||
table_form=table_form,
|
table_form=table_form,
|
||||||
|
|
|
@ -7,6 +7,7 @@ from canaille.models import User
|
||||||
from flask import abort
|
from flask import abort
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
|
from flask import request
|
||||||
from flask import session
|
from flask import session
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
|
@ -84,3 +85,12 @@ def set_parameter_in_url_query(url, **kwargs):
|
||||||
parameters = {**parameters, **kwargs}
|
parameters = {**parameters, **kwargs}
|
||||||
split[3] = "&".join(f"{key}={value}" for key, value in parameters.items())
|
split[3] = "&".join(f"{key}={value}" for key, value in parameters.items())
|
||||||
return urlunsplit(split)
|
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)
|
||||||
|
|
|
@ -8,6 +8,7 @@ from flask_babel import gettext as _
|
||||||
from flask_themer import render_template
|
from flask_themer import render_template
|
||||||
|
|
||||||
from .flaskutils import permissions_needed
|
from .flaskutils import permissions_needed
|
||||||
|
from .flaskutils import render_htmx_template
|
||||||
from .forms import CreateGroupForm
|
from .forms import CreateGroupForm
|
||||||
from .forms import EditGroupForm
|
from .forms import EditGroupForm
|
||||||
from .forms import TableForm
|
from .forms import TableForm
|
||||||
|
@ -24,7 +25,7 @@ def groups(user):
|
||||||
if request.form and request.form.get("page") and not table_form.validate():
|
if request.form and request.form.get("page") and not table_form.validate():
|
||||||
abort(404)
|
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"))
|
@bp.route("/add", methods=("GET", "POST"))
|
||||||
|
@ -96,8 +97,9 @@ def edit_group(group):
|
||||||
else:
|
else:
|
||||||
flash(_("Group edition failed."), "error")
|
flash(_("Group edition failed."), "error")
|
||||||
|
|
||||||
return render_template(
|
return render_htmx_template(
|
||||||
"group.html",
|
"group.html",
|
||||||
|
"partial/users.html",
|
||||||
form=form,
|
form=form,
|
||||||
edited_group=group,
|
edited_group=group,
|
||||||
table_form=table_form,
|
table_form=table_form,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from canaille.flaskutils import permissions_needed
|
from canaille.flaskutils import permissions_needed
|
||||||
|
from canaille.flaskutils import render_htmx_template
|
||||||
from canaille.forms import TableForm
|
from canaille.forms import TableForm
|
||||||
from canaille.oidc.models import AuthorizationCode
|
from canaille.oidc.models import AuthorizationCode
|
||||||
from flask import abort
|
from flask import abort
|
||||||
|
@ -17,7 +18,7 @@ def index(user):
|
||||||
if request.form and request.form.get("page") and not table_form.validate():
|
if request.form and request.form.get("page") and not table_form.validate():
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
return render_template(
|
return render_htmx_template(
|
||||||
"oidc/admin/authorization_list.html",
|
"oidc/admin/authorization_list.html",
|
||||||
menuitem="admin",
|
menuitem="admin",
|
||||||
table_form=table_form,
|
table_form=table_form,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from canaille.flaskutils import permissions_needed
|
from canaille.flaskutils import permissions_needed
|
||||||
|
from canaille.flaskutils import render_htmx_template
|
||||||
from canaille.forms import TableForm
|
from canaille.forms import TableForm
|
||||||
from canaille.oidc.forms import ClientAdd
|
from canaille.oidc.forms import ClientAdd
|
||||||
from canaille.oidc.models import Client
|
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():
|
if request.form and request.form.get("page") and not table_form.validate():
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
return render_template(
|
return render_htmx_template(
|
||||||
"oidc/admin/client_list.html", menuitem="admin", table_form=table_form
|
"oidc/admin/client_list.html", menuitem="admin", table_form=table_form
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from canaille.flaskutils import permissions_needed
|
from canaille.flaskutils import permissions_needed
|
||||||
|
from canaille.flaskutils import render_htmx_template
|
||||||
from canaille.forms import TableForm
|
from canaille.forms import TableForm
|
||||||
from canaille.models import User
|
from canaille.models import User
|
||||||
from canaille.oidc.models import Client
|
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():
|
if request.form and request.form.get("page") and not table_form.validate():
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
return render_template(
|
return render_htmx_template(
|
||||||
"oidc/admin/token_list.html", menuitem="admin", table_form=table_form
|
"oidc/admin/token_list.html", menuitem="admin", table_form=table_form
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{% extends theme('base.html') %}
|
{% extends theme('base.html') %}
|
||||||
{% import 'macro/fomanticui.html' as sui %}
|
{% import 'macro/fomanticui.html' as sui %}
|
||||||
|
{% import "macro/table.html" as table %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script src="/static/js/confirm.js"></script>
|
<script src="/static/js/confirm.js"></script>
|
||||||
|
@ -87,6 +88,9 @@
|
||||||
</div>
|
</div>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
<div class="ui attached segment">
|
||||||
|
{{ table.search(table_form, "table.users") }}
|
||||||
|
</div>
|
||||||
{% include "partial/users.html" %}
|
{% include "partial/users.html" %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
</div>
|
</div>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="ui attached segment">
|
<div class="ui attached segment">
|
||||||
{{ table.search(table_form) }}
|
{{ table.search(table_form, "table.groups") }}
|
||||||
</div>
|
</div>
|
||||||
{% include "partial/groups.html" %}
|
{% include "partial/groups.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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 id="search" action="{{ url_for(request.url_rule.endpoint, **request.view_args) }}" method="POST" class="ui form">
|
||||||
{{ form.hidden_tag() if form.hidden_tag }}
|
{{ form.hidden_tag() if form.hidden_tag }}
|
||||||
<input type="hidden" name="page" value="{{ form.page.data }}">
|
<input type="hidden" name="page" value="{{ form.page.data }}">
|
||||||
<div class="ui fluid action input">
|
<div class="ui fluid action input icon">
|
||||||
<input type="search" placeholder="{{ _("Search…") }}" name="{{ form.query.name }}" value="{{ form.query.data }}">
|
<input
|
||||||
<button type="submit" class="ui icon button" title="{{ _("Search") }}">
|
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>
|
<i class="search icon"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro pagination(form) %}
|
{# At the moment we need to build one form per button
|
||||||
<form id="pagination" action="{{ url_for(request.url_rule.endpoint, **request.view_args) }}" method="POST" class="ui form">
|
# 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 }}
|
{{ form.hidden_tag() if form.hidden_tag }}
|
||||||
|
<input type="hidden" name="page" value="{{ page }}">
|
||||||
<input type="hidden" name="query" value="{{ form.query.data }}">
|
<input type="hidden" name="query" value="{{ form.query.data }}">
|
||||||
<div class="ui right floated stackable buttons">
|
<button
|
||||||
<span class="icon disabled ui button">
|
type="submit"
|
||||||
{% trans %}Page{% endtrans %}
|
style="border-radius: 0"
|
||||||
</span>
|
class="icon ui button"
|
||||||
{% if form.page.data > 1 %}
|
hx-post="{{ url_for(request.url_rule.endpoint, **request.view_args) }}"
|
||||||
<button name="page" type="submit" class="icon ui button" value="{{ form.page.data - 1 }}">
|
hx-target="closest table"
|
||||||
<i class="left chevron icon"></i>
|
hx-swap="outerHTML"
|
||||||
</button>
|
>
|
||||||
{% else %}
|
{{ caller() }}
|
||||||
<span class="icon disabled ui button">
|
</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>
|
|
||||||
</form>
|
</form>
|
||||||
{% endmacro %}
|
{% 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 %}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="ui attached segment">
|
<div class="ui attached segment">
|
||||||
{{ table.search(table_form) }}
|
{{ table.search(table_form, "table.codes") }}
|
||||||
</div>
|
</div>
|
||||||
{% include "partial/oidc/admin/authorization_list.html" %}
|
{% include "partial/oidc/admin/authorization_list.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ui attached segment">
|
<div class="ui attached segment">
|
||||||
{{ table.search(table_form) }}
|
{{ table.search(table_form, "table.clients") }}
|
||||||
</div>
|
</div>
|
||||||
{% include "partial/oidc/admin/client_list.html" %}
|
{% include "partial/oidc/admin/client_list.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="ui attached segment">
|
<div class="ui attached segment">
|
||||||
{{ table.search(table_form) }}
|
{{ table.search(table_form, "table.tokens") }}
|
||||||
</div>
|
</div>
|
||||||
{% include "partial/oidc/admin/token_list.html" %}
|
{% include "partial/oidc/admin/token_list.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui attached segment">
|
<div class="ui attached segment">
|
||||||
{{ table.search(table_form) }}
|
{{ table.search(table_form, "table.users") }}
|
||||||
</div>
|
</div>
|
||||||
{% include "partial/users.html" %}
|
{% include "partial/users.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -105,10 +105,13 @@
|
||||||
<footer>
|
<footer>
|
||||||
<a href="{{ url_for('account.about') }}">{{ _("About Canaille") }}</a>
|
<a href="{{ url_for('account.about') }}">{{ _("About Canaille") }}</a>
|
||||||
</footer>
|
</footer>
|
||||||
<script src="/static/htmx/htmx.min.js"></script>
|
|
||||||
<script src="/static/jquery/jquery.min.js"></script>
|
<script src="/static/jquery/jquery.min.js"></script>
|
||||||
<script src="/static/fomanticui/semantic.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 src="/static/js/base.js"></script>
|
||||||
|
<script>
|
||||||
|
htmx.config.requestClass = "loading"
|
||||||
|
</script>
|
||||||
{% block script %}{% endblock %}
|
{% block script %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in a new issue