Compare commits
3 commits
0c2a9847cb
...
52bd0d4c91
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52bd0d4c91 | ||
|
|
d55539192e | ||
|
|
bdec7202df |
4 changed files with 99 additions and 20 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
{% macro options(items, value_attr="id", label_path="name", getp=None) -%}
|
{% macro options(items, value_attr="id", label_path="name", getp=None) -%}
|
||||||
|
|
||||||
{%- for obj in items -%}
|
{%- for obj in items -%}
|
||||||
<option value="{{ getp(obj, value_attr) }}">{{ getp(obj, label_path) }}</option>
|
<option value="{{ getp(obj, value_attr) }}">{{ getp(obj, label_path) }}</option>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
|
|
@ -19,9 +20,9 @@
|
||||||
|
|
||||||
{% macro rows(items, fields, getp=None) -%}
|
{% macro rows(items, fields, getp=None) -%}
|
||||||
{%- for obj in items -%}
|
{%- for obj in items -%}
|
||||||
<tr id="row-{{ obj.id }}">
|
<tr id="row-{{ obj.id }}" style="cursor: pointer;">
|
||||||
{%- for f in fields -%}
|
{%- for f in fields -%}
|
||||||
<td data-field="{{ f }}">{{ getp(obj, f) }}</td>
|
<td data-field="{{ f }}" class="text-nowrap">{{ getp(obj, f) if getp(obj, f) is not none else '-' }}</td>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</tr>
|
</tr>
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
|
|
@ -32,14 +33,54 @@
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro pager(model, page, pages, per_page, sort, filters, fields_csv) -%}
|
{% macro pager(model, page, pages, per_page, sort, filters, fields_csv) -%}
|
||||||
<nav id="pager" hx-swap-oob="true">
|
{#
|
||||||
<a hx-get="/ui/{{ model }}/frag/rows?page={{ page-1 }}&per_page={{ per_page }}{% if sort %}&sort={{ sort }}{% endif %}{% if fields_csv %}&fields_csv={{ fields_csv|urlencode }}{% endif %}{% for k,v in filters.items() %}&{{k}}={{ v|urlencode }}{% endfor %}"
|
<nav id="pager">
|
||||||
hx-target="#rows" rel="prev" hx-swap="innerHTML">Prev</a>
|
{% if page > 1 %}
|
||||||
|
<button type="button"
|
||||||
|
hx-get="/ui/{{ model }}/frag/rows?page=1&per_page={{ per_page }}{% if sort %}&sort={{ sort }}{% endif %}{% if fields_csv %}&fields_csv={{ fields_csv|urlencode }}{% endif %}{% for k,v in filters.items() %}&{{k}}={{ v|urlencode }}{% endfor %}"
|
||||||
|
hx-target="#rows" hx-swap="innerHTML">First</button>
|
||||||
|
<button type="button"
|
||||||
|
hx-get="/ui/{{ model }}/frag/rows?page={{ page-1 }}&per_page={{ per_page }}{% if sort %}&sort={{ sort }}{% endif %}{% if fields_csv %}&fields_csv={{ fields_csv|urlencode }}{% endif %}{% for k,v in filters.items() %}&{{k}}={{ v|urlencode }}{% endfor %}"
|
||||||
|
hx-target="#rows" rel="prev" hx-swap="innerHTML">Prev</button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<span>Page {{ page }} / {{ pages }}</span>
|
<span>Page {{ page }} / {{ pages }}</span>
|
||||||
|
|
||||||
<a hx-get="/ui/{{ model }}/frag/rows?page={{ page+1 }}&per_page={{ per_page }}{% if sort %}&sort={{ sort }}{% endif %}{% if fields_csv %}&fields_csv={{ fields_csv|urlencode }}{% endif %}{% for k,v in filters.items() %}&{{k}}={{ v|urlencode }}{% endfor %}"
|
{% if page < pages %} <button type="button"
|
||||||
hx-target="#rows" rel="next" hx-swap="innerHTML">Next</a>
|
hx-get="/ui/{{ model }}/frag/rows?page={{ page+1 }}&per_page={{ per_page }}{% if sort %}&sort={{ sort }}{% endif %}{% if fields_csv %}&fields_csv={{ fields_csv|urlencode }}{% endif %}{% for k,v in filters.items() %}&{{k}}={{ v|urlencode }}{% endfor %}"
|
||||||
|
hx-target="#rows" rel="next" hx-swap="innerHTML">Next</button>
|
||||||
|
<button type="button"
|
||||||
|
hx-get="/ui/{{ model }}/frag/rows?page={{ pages }}&per_page={{ per_page }}{% if sort %}&sort={{ sort }}{% endif %}{% if fields_csv %}&fields_csv={{ fields_csv|urlencode }}{% endif %}{% for k,v in filters.items() %}&{{k}}={{ v|urlencode }}{% endfor %}"
|
||||||
|
hx-target="#rows" hx-swap="innerHTML">Last</button>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
|
#}
|
||||||
|
{% set p = page|int %}
|
||||||
|
{% set pg = pages|int %}
|
||||||
|
{% set prev = [1, p-1]|max %}
|
||||||
|
{% set nxt = [pg, p+1]|min %}
|
||||||
|
|
||||||
|
<nav class="mt-3" aria-label="Pagination">
|
||||||
|
<ul class="pagination justify-content-center bg-white">
|
||||||
|
<li class="page-item{{ ' disabled' if p <= 1 }}">
|
||||||
|
<a class="page-link"
|
||||||
|
hx-get="/ui/{{ model }}/frag/rows?page={{ prev }}&per_page={{ per_page }}{% if sort %}&sort={{ sort }}{% endif %}{% if fields_csv %}&fields_csv={{ fields_csv|urlencode }}{% endif %}{% for k,v in (filters or {}).items() %}&{{k}}={{ v|urlencode }}{% endfor %}"
|
||||||
|
hx-target="#rows" hx-swap="innerHTML"
|
||||||
|
hx-on:click="document.querySelector('#pager-state input[name=page]').value='{{ prev }}'">
|
||||||
|
Prev
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="page-item{{ ' disabled' if p >= pg }}">
|
||||||
|
<a class="page-link"
|
||||||
|
hx-get="/ui/{{ model }}/frag/rows?page={{ nxt }}&per_page={{ per_page }}{% if sort %}&sort={{ sort }}{% endif %}{% if fields_csv %}&fields_csv={{ fields_csv|urlencode }}{% endif %}{% for k,v in (filters or {}).items() %}&{{k}}={{ v|urlencode }}{% endfor %}"
|
||||||
|
hx-target="#rows" hx-swap="innerHTML"
|
||||||
|
hx-on:click="document.querySelector('#pager-state input[name=page]').value='{{ nxt }}'">
|
||||||
|
Next
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<span>Page {{ p }} / {{ pg }}</span>
|
||||||
</nav>
|
</nav>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ from ..service import CrudService
|
||||||
from ..eager import default_eager_policy
|
from ..eager import default_eager_policy
|
||||||
from .type_map import build_form_schema
|
from .type_map import build_form_schema
|
||||||
|
|
||||||
|
Session = None
|
||||||
|
|
||||||
def make_fragments_blueprint(db_session_factory, registry: Dict[str, Any], *, name="frags"):
|
def make_fragments_blueprint(db_session_factory, registry: Dict[str, Any], *, name="frags"):
|
||||||
"""
|
"""
|
||||||
HTML fragments for HTMX/Alpine. No base pages. Pure partials:
|
HTML fragments for HTMX/Alpine. No base pages. Pure partials:
|
||||||
|
|
@ -20,8 +22,18 @@ def make_fragments_blueprint(db_session_factory, registry: Dict[str, Any], *, na
|
||||||
GET /<model>/frag/rows -> <tr>...</tr> + pager markup if wanted
|
GET /<model>/frag/rows -> <tr>...</tr> + pager markup if wanted
|
||||||
GET /<model>/frag/form -> <form>...</form> (auto-generated)
|
GET /<model>/frag/form -> <form>...</form> (auto-generated)
|
||||||
"""
|
"""
|
||||||
|
global Session
|
||||||
|
if Session is None:
|
||||||
|
Session = scoped_session(db_session_factory)
|
||||||
|
|
||||||
bp = Blueprint(name, __name__, template_folder="templates")
|
bp = Blueprint(name, __name__, template_folder="templates")
|
||||||
def session(): return scoped_session(db_session_factory)()
|
|
||||||
|
def session():
|
||||||
|
return Session
|
||||||
|
|
||||||
|
@bp.teardown_app_request
|
||||||
|
def remove_session(exc=None):
|
||||||
|
Session.remove()
|
||||||
|
|
||||||
def _parse_filters(args):
|
def _parse_filters(args):
|
||||||
reserved = {"page", "per_page", "sort", "expand", "fields", "value", "label", "label_tpl", "fields_csv", "li_label", "li_sublabel"}
|
reserved = {"page", "per_page", "sort", "expand", "fields", "value", "label", "label_tpl", "fields_csv", "li_label", "li_sublabel"}
|
||||||
|
|
@ -106,14 +118,32 @@ def make_fragments_blueprint(db_session_factory, registry: Dict[str, Any], *, na
|
||||||
expand = _collect_expand_from_paths(fields)
|
expand = _collect_expand_from_paths(fields)
|
||||||
spec = QuerySpec(filters=filters, order_by=[sort] if sort else [], page=page, per_page=per_page, expand=expand)
|
spec = QuerySpec(filters=filters, order_by=[sort] if sort else [], page=page, per_page=per_page, expand=expand)
|
||||||
s = session(); svc = CrudService(s, default_eager_policy)
|
s = session(); svc = CrudService(s, default_eager_policy)
|
||||||
rows, total = svc.list(Model, spec)
|
rows, _ = svc.list(Model, spec)
|
||||||
|
|
||||||
|
html = render_template("crudkit/rows.html", items=rows, fields=fields, getp=_getp)
|
||||||
|
|
||||||
|
return html
|
||||||
|
|
||||||
|
@bp.get("/<model>/frag/pager")
|
||||||
|
def pager(model):
|
||||||
|
Model = registry.get(model) or abort(404)
|
||||||
|
page = request.args.get("page", type=int) or 1
|
||||||
|
print(page)
|
||||||
|
per_page = request.args.get("per_page", type=int) or 20
|
||||||
|
filters = _parse_filters(request.args)
|
||||||
|
sort = request.args.get("sort")
|
||||||
|
fields_csv = request.args.get("fields_csv") or "id,name"
|
||||||
|
fields = _paths_from_csv(fields_csv)
|
||||||
|
expand = _collect_expand_from_paths(fields)
|
||||||
|
|
||||||
|
spec = QuerySpec(filters=filters, order_by=[sort] if sort else [], page=page, per_page=per_page, expand=expand)
|
||||||
|
s = session(); svc = CrudService(s, default_eager_policy)
|
||||||
|
_, total = svc.list(Model, spec)
|
||||||
pages = max(1, ceil(total / per_page))
|
pages = max(1, ceil(total / per_page))
|
||||||
|
|
||||||
rows_html = render_template("crudkit/rows.html", items=rows, fields=fields, getp=_getp)
|
html = render_template("crudkit/pager.html", model=model, page=page, pages=pages,
|
||||||
pager_html = render_template("crudkit/pager.html", model=model, page=page, pages=pages,
|
|
||||||
per_page=per_page, sort=sort, filters=filters, fields_csv=fields_csv)
|
per_page=per_page, sort=sort, filters=filters, fields_csv=fields_csv)
|
||||||
|
return html
|
||||||
return rows_html + pager_html
|
|
||||||
|
|
||||||
@bp.get("/<model>/frag/form")
|
@bp.get("/<model>/frag/form")
|
||||||
def form(model):
|
def form(model):
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro dynamic_table(id, headers=none, fields=none, entry_route=None, title=None, page=1, per_page=15, offset=0,
|
{% macro dynamic_table(id, headers=none, fields=none, entry_route=None, title=None, page=1, per_page=15, offset=0,
|
||||||
refresh_url=none, model=none) %}
|
refresh_url=none, model=none, sort=none) %}
|
||||||
<!-- Table Fragment -->
|
<!-- Table Fragment -->
|
||||||
|
|
||||||
{% if title %}
|
{% if title %}
|
||||||
|
|
@ -76,12 +76,19 @@ refresh_url=none, model=none) %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="rows"
|
<tbody id="rows"
|
||||||
hx-get="/ui/{{ model }}/frag/rows?page={{ page }}&{{ per_page }}&fields_csv={{ fields|join(',') }}"
|
hx-get="/ui/{{ model }}/frag/rows?page={{ page }}&per_page={{ per_page }}&fields_csv={{ fields|join(',') }}"
|
||||||
hx-trigger="load"
|
hx-trigger="load" hx-target="#rows" hx-swap="innerHTML"></tbody>
|
||||||
hx-target="#rows"
|
|
||||||
hx-swap="innerHTML">
|
|
||||||
</tbody>
|
|
||||||
</table>
|
</table>
|
||||||
<div id="pager"></div>
|
<div id="pager-state">
|
||||||
|
<input type="hidden" name="page" value="{{ page }}">
|
||||||
|
<input type="hidden" name="per_page" value="{{ per_page }}">
|
||||||
|
<input type="hidden" name="fields_csv" value="{{ fields|join(',') }}">
|
||||||
|
{% if sort %}<input type="hidden" name="sort" value="{{ sort }}">{% endif %}
|
||||||
|
{% for k,v in (filters or {}).items() %}
|
||||||
|
<input type="hidden" name="{{ k }}" value="{{ v }}">
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="pager" hx-get="/ui/{{ model }}/frag/pager" hx-include="#pager-state"
|
||||||
|
hx-trigger="load, htmx:afterSwap from:#rows" hx-target="#pager" hx-swap="innerHTML"></div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
@ -87,6 +87,7 @@
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/htmx.org@1.9.12/dist/ext/debug.js"></script>
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const searchInput = document.querySelector('#search');
|
const searchInput = document.querySelector('#search');
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue