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
|
|
@ -1,31 +1,115 @@
|
|||
function Table(cfg) {
|
||||
return {
|
||||
id: cfg.id,
|
||||
refreshUrl: cfg.refreshUrl,
|
||||
headers: cfg.headers || [],
|
||||
perPage: cfg.perPage || 10,
|
||||
offset: cfg.offset || 0,
|
||||
fields: cfg.fields || [],
|
||||
return {
|
||||
id: cfg.id,
|
||||
refreshUrl: cfg.refreshUrl,
|
||||
headers: cfg.headers || [],
|
||||
fields: cfg.fields || [],
|
||||
// external API
|
||||
perPage: cfg.perPage || 10,
|
||||
offset: cfg.offset || 0,
|
||||
|
||||
init() {
|
||||
if (this.refreshUrl) this.refresh();
|
||||
},
|
||||
// derived + server-fed state
|
||||
page: Math.floor((cfg.offset || 0) / (cfg.perPage || 10)) + 1,
|
||||
total: 0,
|
||||
pages: 0,
|
||||
|
||||
buildRefreshUrl() {
|
||||
if (!this.refreshUrl) return null;
|
||||
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();
|
||||
return u.toString();
|
||||
},
|
||||
init() {
|
||||
if (this.refreshUrl) this.refresh();
|
||||
},
|
||||
|
||||
async refresh() {
|
||||
const url = this.buildRefreshUrl();
|
||||
if (!url) return;
|
||||
const res = await fetch(url, { headers: { 'HX-Request': 'true' } });
|
||||
const text = await res.text();
|
||||
if (this.$refs.body) {
|
||||
this.$refs.body.innerHTML = text;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
buildRefreshUrl() {
|
||||
if (!this.refreshUrl) return null;
|
||||
const u = new URL(this.refreshUrl, window.location.origin);
|
||||
|
||||
// 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();
|
||||
},
|
||||
|
||||
async refresh() {
|
||||
const url = this.buildRefreshUrl();
|
||||
if (!url) return;
|
||||
const res = await fetch(url, { headers: { 'X-Requested-With': 'fetch' } });
|
||||
const html = await res.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)));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,4 @@
|
|||
<td class="text-nowrap">{{ val if val else '-' }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{% if pages > 1 %}
|
||||
<tr>
|
||||
<td colspan="{{ rows[0]|length - 1 }}">
|
||||
Page {{ page }} of {{ pages }} ({{ total }} total)
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
<tbody>
|
||||
{% for row in rows %}
|
||||
<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 %}
|
||||
<td class="text-nowrap{% if cell.type=='bool' %} text-center{% endif %}">
|
||||
{% if cell.type == 'bool' %}
|
||||
|
|
@ -40,9 +40,9 @@
|
|||
document.addEventListener("DOMContentLoaded", function () {
|
||||
new DataTable('#datatable-{{ id|default('table')|replace(' ', ' - ')|lower }}', {
|
||||
pageLength: {{ per_page }},
|
||||
scrollX: true,
|
||||
scrollY: '60vh',
|
||||
scrollCollapse: true,
|
||||
scrollX: true,
|
||||
scrollY: '60vh',
|
||||
scrollCollapse: true,
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
|
@ -51,7 +51,8 @@
|
|||
{% endif %}
|
||||
{% 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 -->
|
||||
|
||||
{% if rows or refresh_url %}
|
||||
|
|
@ -67,7 +68,18 @@
|
|||
fields: {{ fields|tojson if fields else "[]" }}
|
||||
})'>
|
||||
<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">
|
||||
<tr>
|
||||
{% for h in headers %}
|
||||
|
|
@ -77,11 +89,8 @@
|
|||
</thead>
|
||||
<tbody x-ref="body"></tbody>
|
||||
</table>
|
||||
<nav>
|
||||
<ul class="pagination"></ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="container text-center">No data.</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{% endmacro %}
|
||||
|
|
@ -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.exc import IntegrityError
|
||||
from sqlalchemy.sql import Select
|
||||
|
|
@ -183,14 +183,21 @@ def list_items(model_name):
|
|||
if want_list:
|
||||
return render_template("fragments/_list_fragment.html", options=items)
|
||||
if want_table:
|
||||
return render_template("fragments/_table_data_fragment.html",
|
||||
rows=items,
|
||||
model_name=model_name,
|
||||
total=total,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
pages=(0 if unlimited else ((total + per_page - 1) // per_page)),
|
||||
)
|
||||
# return render_template("fragments/_table_data_fragment.html",
|
||||
# rows=items,
|
||||
# model_name=model_name,
|
||||
# total=total,
|
||||
# page=page,
|
||||
# per_page=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({
|
||||
"items": items,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue