Enhance table pagination: update URL building for server-side pagination, improve refresh logic, and add pagination metadata handling in response headers.
This commit is contained in:
parent
220bd3e4b6
commit
f3f4493698
4 changed files with 147 additions and 55 deletions
|
|
@ -3,9 +3,15 @@ function Table(cfg) {
|
||||||
id: cfg.id,
|
id: cfg.id,
|
||||||
refreshUrl: cfg.refreshUrl,
|
refreshUrl: cfg.refreshUrl,
|
||||||
headers: cfg.headers || [],
|
headers: cfg.headers || [],
|
||||||
|
fields: cfg.fields || [],
|
||||||
|
// external API
|
||||||
perPage: cfg.perPage || 10,
|
perPage: cfg.perPage || 10,
|
||||||
offset: cfg.offset || 0,
|
offset: cfg.offset || 0,
|
||||||
fields: cfg.fields || [],
|
|
||||||
|
// derived + server-fed state
|
||||||
|
page: Math.floor((cfg.offset || 0) / (cfg.perPage || 10)) + 1,
|
||||||
|
total: 0,
|
||||||
|
pages: 0,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
if (this.refreshUrl) this.refresh();
|
if (this.refreshUrl) this.refresh();
|
||||||
|
|
@ -14,18 +20,96 @@ function Table(cfg) {
|
||||||
buildRefreshUrl() {
|
buildRefreshUrl() {
|
||||||
if (!this.refreshUrl) return null;
|
if (!this.refreshUrl) return null;
|
||||||
const u = new URL(this.refreshUrl, window.location.origin);
|
const u = new URL(this.refreshUrl, window.location.origin);
|
||||||
u.search = new URLSearchParams({ view: 'table', offset: this.offset, 'limit': this.perPage, 'fields': this.fields }).toString();
|
|
||||||
|
// We want server-side pagination with page/per_page
|
||||||
|
u.searchParams.set('view', 'table');
|
||||||
|
u.searchParams.set('page', this.page);
|
||||||
|
u.searchParams.set('per_page', this.perPage);
|
||||||
|
|
||||||
|
// Send requested fields in the way your backend expects
|
||||||
|
// If your route supports &field=... repeaters, do this:
|
||||||
|
this.fields.forEach(f => u.searchParams.append('field', f));
|
||||||
|
// If your route only supports "fields=a,b,c", then use:
|
||||||
|
// if (this.fields.length) u.searchParams.set('fields', this.fields.join(','));
|
||||||
|
|
||||||
return u.toString();
|
return u.toString();
|
||||||
},
|
},
|
||||||
|
|
||||||
async refresh() {
|
async refresh() {
|
||||||
const url = this.buildRefreshUrl();
|
const url = this.buildRefreshUrl();
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
const res = await fetch(url, { headers: { 'HX-Request': 'true' } });
|
const res = await fetch(url, { headers: { 'X-Requested-With': 'fetch' } });
|
||||||
const text = await res.text();
|
const html = await res.text();
|
||||||
if (this.$refs.body) {
|
|
||||||
this.$refs.body.innerHTML = text;
|
// Dump the server-rendered <tr> rows into the tbody
|
||||||
}
|
if (this.$refs.body) this.$refs.body.innerHTML = html;
|
||||||
|
|
||||||
|
// Read pagination metadata from headers
|
||||||
|
const toInt = (v, d=0) => {
|
||||||
|
const n = parseInt(v ?? '', 10);
|
||||||
|
return Number.isFinite(n) ? n : d;
|
||||||
|
};
|
||||||
|
|
||||||
|
const total = toInt(res.headers.get('X-Total'));
|
||||||
|
const pages = toInt(res.headers.get('X-Pages'));
|
||||||
|
const page = toInt(res.headers.get('X-Page'), this.page);
|
||||||
|
const per = toInt(res.headers.get('X-Per-Page'), this.perPage);
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
this.total = total;
|
||||||
|
this.pages = pages;
|
||||||
|
this.page = page;
|
||||||
|
this.perPage = per;
|
||||||
|
this.offset = (this.page - 1) * this.perPage;
|
||||||
|
|
||||||
|
// Update pager UI (if you put <ul x-ref="pagination"> in your caption)
|
||||||
|
this.buildPager();
|
||||||
|
// Caption numbers are bound via x-text so they auto-update.
|
||||||
|
},
|
||||||
|
|
||||||
|
buildPager() {
|
||||||
|
const ul = this.$refs.pagination;
|
||||||
|
if (!ul) return;
|
||||||
|
ul.innerHTML = '';
|
||||||
|
|
||||||
|
const mk = (label, page, disabled=false, active=false) => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = `page-item${disabled ? ' disabled' : ''}${active ? ' active' : ''}`;
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.className = 'page-link';
|
||||||
|
a.href = '#';
|
||||||
|
a.textContent = label;
|
||||||
|
a.onclick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (disabled || active) return;
|
||||||
|
this.page = page;
|
||||||
|
this.refresh();
|
||||||
|
};
|
||||||
|
li.appendChild(a);
|
||||||
|
return li;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prev
|
||||||
|
ul.appendChild(mk('«', Math.max(1, this.page - 1), this.page <= 1));
|
||||||
|
|
||||||
|
// Windowed page buttons
|
||||||
|
const maxButtons = 7;
|
||||||
|
let start = Math.max(1, this.page - Math.floor(maxButtons/2));
|
||||||
|
let end = Math.min(this.pages || 1, start + maxButtons - 1);
|
||||||
|
start = Math.max(1, Math.min(start, Math.max(1, end - maxButtons + 1)));
|
||||||
|
|
||||||
|
if (start > 1) ul.appendChild(mk('1', 1));
|
||||||
|
if (start > 2) ul.appendChild(mk('…', this.page, true));
|
||||||
|
|
||||||
|
for (let p = start; p <= end; p++) {
|
||||||
|
ul.appendChild(mk(String(p), p, false, p === this.page));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (end < (this.pages || 1) - 1) ul.appendChild(mk('…', this.page, true));
|
||||||
|
if (end < (this.pages || 1)) ul.appendChild(mk(String(this.pages), this.pages));
|
||||||
|
|
||||||
|
// Next
|
||||||
|
ul.appendChild(mk('»', Math.min(this.pages || 1, this.page + 1), this.page >= (this.pages || 1)));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -6,11 +6,3 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if pages > 1 %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="{{ rows[0]|length - 1 }}">
|
|
||||||
Page {{ page }} of {{ pages }} ({{ total }} total)
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for row in rows %}
|
{% for row in rows %}
|
||||||
<tr {% if entry_route %}onclick="window.location='{{ url_for('main.' + entry_route, id=row.id) }}'"
|
<tr {% if entry_route %}onclick="window.location='{{ url_for('main.' + entry_route, id=row.id) }}'"
|
||||||
style="cursor: pointer;"{% endif %}{% if row['highlight'] %} class="table-info"{% endif %}>
|
style="cursor: pointer;" {% endif %}{% if row['highlight'] %} class="table-info" {% endif %}>
|
||||||
{% for cell in row.cells %}
|
{% for cell in row.cells %}
|
||||||
<td class="text-nowrap{% if cell.type=='bool' %} text-center{% endif %}">
|
<td class="text-nowrap{% if cell.type=='bool' %} text-center{% endif %}">
|
||||||
{% if cell.type == 'bool' %}
|
{% if cell.type == 'bool' %}
|
||||||
|
|
@ -51,7 +51,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro dynamic_table(id, headers=none, fields=none, entry_route=None, title=None, per_page=15, offset=0, refresh_url=none) %}
|
{% macro dynamic_table(id, headers=none, fields=none, entry_route=None, title=None, per_page=15, offset=0,
|
||||||
|
refresh_url=none) %}
|
||||||
<!-- Table Fragment -->
|
<!-- Table Fragment -->
|
||||||
|
|
||||||
{% if rows or refresh_url %}
|
{% if rows or refresh_url %}
|
||||||
|
|
@ -67,7 +68,18 @@
|
||||||
fields: {{ fields|tojson if fields else "[]" }}
|
fields: {{ fields|tojson if fields else "[]" }}
|
||||||
})'>
|
})'>
|
||||||
<table id="datatable-{{ id|default('table')|replace(' ', '-')|lower }}"
|
<table id="datatable-{{ id|default('table')|replace(' ', '-')|lower }}"
|
||||||
class="table table-bordered table-sm table-hover table-striped table-light m-0{% if title %} caption-top{% endif %}">
|
class="table table-bordered table-sm table-hover table-striped table-light m-0 caption-bottom">
|
||||||
|
<caption class="p-0">
|
||||||
|
<nav class="d-flex flex-column align-items-center px-2 py-1">
|
||||||
|
<!-- This is your pagination control -->
|
||||||
|
<ul class="pagination mb-0" x-ref="pagination"></ul>
|
||||||
|
<!-- This is just Alpine text binding -->
|
||||||
|
<div>
|
||||||
|
Page <span x-text="page"></span> of <span x-text="pages"></span>
|
||||||
|
(<span x-text="total"></span> total)
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</caption>
|
||||||
<thead class="sticky-top">
|
<thead class="sticky-top">
|
||||||
<tr>
|
<tr>
|
||||||
{% for h in headers %}
|
{% for h in headers %}
|
||||||
|
|
@ -77,9 +89,6 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody x-ref="body"></tbody>
|
<tbody x-ref="body"></tbody>
|
||||||
</table>
|
</table>
|
||||||
<nav>
|
|
||||||
<ul class="pagination"></ul>
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="container text-center">No data.</div>
|
<div class="container text-center">No data.</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from flask import Blueprint, request, render_template, jsonify, abort
|
from flask import Blueprint, request, render_template, jsonify, abort, make_response
|
||||||
from sqlalchemy.engine import ScalarResult
|
from sqlalchemy.engine import ScalarResult
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.sql import Select
|
from sqlalchemy.sql import Select
|
||||||
|
|
@ -183,14 +183,21 @@ def list_items(model_name):
|
||||||
if want_list:
|
if want_list:
|
||||||
return render_template("fragments/_list_fragment.html", options=items)
|
return render_template("fragments/_list_fragment.html", options=items)
|
||||||
if want_table:
|
if want_table:
|
||||||
return render_template("fragments/_table_data_fragment.html",
|
# return render_template("fragments/_table_data_fragment.html",
|
||||||
rows=items,
|
# rows=items,
|
||||||
model_name=model_name,
|
# model_name=model_name,
|
||||||
total=total,
|
# total=total,
|
||||||
page=page,
|
# page=page,
|
||||||
per_page=per_page,
|
# per_page=per_page,
|
||||||
pages=(0 if unlimited else ((total + per_page - 1) // per_page)),
|
# pages=(0 if unlimited else ((total + per_page - 1) // per_page)),
|
||||||
)
|
# )
|
||||||
|
resp = make_response(render_template("fragments/_table_data_fragment.html",
|
||||||
|
rows=items, model_name=model_name))
|
||||||
|
resp.headers['X-Total'] = str(total)
|
||||||
|
resp.headers['X-Page'] = str(page)
|
||||||
|
resp.headers['X-PAges'] = str((0 if unlimited else ((total + per_page - 1) // per_page)))
|
||||||
|
resp.headers['X-Per-Page'] = str(per_page)
|
||||||
|
return resp
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"items": items,
|
"items": items,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue