")
@@ -21,12 +19,15 @@ def init_listing_routes(app):
abort(404)
# read query args
- limit = request.args.get("limit", None)
- limit = int(limit) if (limit is not None and str(limit).isdigit()) else 15
- sort = request.args.get("sort")
- fields_qs = request.args.get("fields")
- cursor = request.args.get("cursor")
- key, _desc, backward = decode_cursor(cursor)
+ # accept both per_page and limit; per_page wins if both provided
+ per_page_qs = request.args.get("per_page")
+ limit_qs = request.args.get("limit")
+ page = int(request.args.get("page", 1) or 1)
+ per_page = int(per_page_qs) if (per_page_qs and per_page_qs.isdigit()) else (
+ int(limit_qs) if (limit_qs and limit_qs.isdigit()) else 15
+ )
+ sort = request.args.get("sort")
+ fields_qs = request.args.get("fields")
# base spec per model
spec = {}
@@ -34,16 +35,8 @@ def init_listing_routes(app):
row_classes = []
if model.lower() == 'inventory':
spec = {"fields": [
- "label",
- "name",
- "barcode",
- "serial",
- "brand.name",
- "model",
- "device_type.description",
- "condition",
- "owner.label",
- "location.label",
+ "label", "name", "barcode", "serial", "brand.name", "model",
+ "device_type.description", "condition", "owner.label", "location.label",
]}
columns = [
{"field": "label"},
@@ -60,14 +53,9 @@ def init_listing_routes(app):
]
elif model.lower() == 'user':
spec = {"fields": [
- "label",
- "last_name",
- "first_name",
- "supervisor.label",
- "robot.overlord",
- "staff",
- "active",
- ], "sort": "first_name,last_name"} # default for users
+ "label", "last_name", "first_name", "supervisor.label",
+ "robot.overlord", "staff", "active",
+ ], "sort": "first_name,last_name"}
columns = [
{"field": "label", "label": "Full Name"},
{"field": "last_name"},
@@ -86,11 +74,7 @@ def init_listing_routes(app):
]
elif model.lower() == 'worklog':
spec = {"fields": [
- "work_item.label",
- "contact.label",
- "start_time",
- "end_time",
- "complete",
+ "work_item.label", "contact.label", "start_time", "end_time", "complete",
]}
columns = [
{"field": "work_item.label", "label": "Work Item",
@@ -106,44 +90,106 @@ def init_listing_routes(app):
{"when": {"field": "complete", "is": False}, "class": "table-danger"}
]
- # Build params to feed CRUDService (flat dict; parse_filters expects flat keys)
+ # Build params to feed CRUDService
params = dict(spec)
- # overlay fields from query (?fields=...)
if fields_qs:
params["fields"] = [p.strip() for p in fields_qs.split(",") if p.strip()]
-
- # overlay sort from query (?sort=...)
if sort:
params["sort"] = sort
- # limit semantics: 0 means "unlimited" in your service layer
- params["limit"] = limit
-
- # forward *all other* query params as filters (flat), excluding known control keys
- CONTROL_KEYS = {"limit", "cursor", "sort", "fields"}
+ # forward remaining query params as filters (flat), excluding control keys
+ CONTROL_KEYS = {"page", "per_page", "limit", "sort", "fields"}
for k, v in request.args.items():
- if k in CONTROL_KEYS:
- continue
- if v is None or v == "":
+ if k in CONTROL_KEYS or v in (None, ""):
continue
params[k] = v
service = crudkit.crud.get_service(cls)
- window = service.seek_window(params, key=key, backward=backward, include_total=True)
+ # Use page-based pagination from the service
+ result = service.page(params, page=page, per_page=per_page, include_total=True)
+ items = result["items"]
- table = render_table(window.items, columns=columns,
+ table = render_table(items, columns=columns,
opts={"object_class": model, "row_classes": row_classes})
+ def _base_params():
+ keep = {}
+ for k, v in request.args.items():
+ if k == "page" or v in (None, ""):
+ continue
+ keep[k] = v
+ # keep both for compatibility; per_page wins in the service anyway
+ keep["per_page"] = per_page
+ keep["limit"] = per_page
+ return keep
+
+ total = int(result["total"] or 0)
+ pages = int(result["pages"] or 1)
+ page = int(result["page"] or 1)
+
+ has_prev = page > 1
+ has_next = page < pages
+
+ base = _base_params()
+ prev_url = url_for("listing.show_list", model=model, **{**base, "page": max(1, page - 1)})
+ next_url = url_for("listing.show_list", model=model, **{**base, "page": min(pages, page + 1)})
+
+ def page_url(n: int) -> str:
+ return url_for("listing.show_list", model=model, **{**base, "page": n})
+
+ def build_nav(page: int, pages: int, window: int = 2):
+ """
+ Returns a list like:
+ [{'type':'page','n':1,'url':'...','active':False}, {'type':'ellipsis'}, ...]
+ Shows first, last, current±window, with ellipses where gaps exist.
+ """
+ if pages <= 1:
+ return [{'type': 'page', 'n': 1, 'url': page_url(1), 'active': True}]
+
+ show = set([1, pages])
+ for n in range(max(1, page - window), min(pages, page + window) + 1):
+ show.add(n)
+
+ out = []
+ last = 0
+ for n in range(1, pages + 1):
+ if n in show:
+ out.append({'type': 'page', 'n': n, 'url': page_url(n), 'active': (n == page)})
+ last = n
+ else:
+ # insert a single ellipsis per gap
+ if last != -1:
+ out.append({'type': 'ellipsis'})
+ last = -1
+ # skip the interior of the gap
+ # we let the for loop continue
+ # collapse any duplicate ellipses at ends (paranoia)
+ cleaned = []
+ for i, item in enumerate(out):
+ if item['type'] == 'ellipsis' and (i == 0 or out[i-1]['type'] == 'ellipsis'):
+ continue
+ cleaned.append(item)
+ if cleaned and cleaned[-1]['type'] == 'ellipsis':
+ cleaned.pop()
+ if cleaned and cleaned[0]['type'] == 'ellipsis':
+ cleaned.pop(0)
+ return cleaned
+
pagination_ctx = {
- "limit": window.limit,
- "total": window.total,
- "next_cursor": encode_cursor(window.last_key, list(window.order.desc), backward=False),
- "prev_cursor": encode_cursor(window.first_key, list(window.order.desc), backward=True),
- "sort": params.get("sort") # expose current sort to the template
+ "page": page,
+ "per_page": per_page,
+ "total": total,
+ "pages": pages,
+ "has_prev": has_prev,
+ "has_next": has_next,
+ "prev_url": prev_url,
+ "next_url": next_url,
+ "nav": build_nav(page, pages, window=2), # tweak window=2..3 to taste
+ "sort": params.get("sort")
}
return render_template("listing.html", model=model, table=table, pagination=pagination_ctx)
- app.register_blueprint(bp_listing)
\ No newline at end of file
+ app.register_blueprint(bp_listing)
diff --git a/inventory/routes/search.py b/inventory/routes/search.py
index 33d23d2..249f262 100644
--- a/inventory/routes/search.py
+++ b/inventory/routes/search.py
@@ -76,7 +76,7 @@ def init_search_routes(app):
{"field": "updates", "format": lambda x: len(x)},
]
worklog_results = worklog_service.list({
- 'contact.label|work_item.label__icontains': q,
+ 'contact.label|work_item.label|updates.content__icontains': q,
'fields': [
"contact.label",
"work_item.label",
diff --git a/inventory/templates/listing.html b/inventory/templates/listing.html
index 9483c18..23a5723 100644
--- a/inventory/templates/listing.html
+++ b/inventory/templates/listing.html
@@ -8,15 +8,49 @@ Inventory Manager - {{ model|title }} Listing
{{ model|title }} Listing
-
+
{{ table | safe }}
-
-
- {{ pagination['total'] }} records
-
-
+
+
+
+ Page {{ pagination.page }} of {{ pagination.pages }} · {{ pagination.total }} records
+
{% endblock %}
\ No newline at end of file
diff --git a/inventory/templates/submit_button.html b/inventory/templates/submit_button.html
index 08cdef1..1025b01 100644
--- a/inventory/templates/submit_button.html
+++ b/inventory/templates/submit_button.html
@@ -90,7 +90,9 @@
if (reply.status === 'success') {
if (!hasId && reply.id) {
window.queueToast('Created successfully.', 'success');
- window.location.href - `/entry/${model}/${reply.id}`;
+ window.newDrafts = [];
+ window.deletedIds = [];
+ window.location.assign(`/entry/${model}/${reply.id}`);
return;
} else {
window.queueToast('Updated successfully.', 'success');
@@ -101,15 +103,12 @@
if (li) li.remove();
}
}
+
+ window.newDrafts = [];
+ window.deletedIds = [];
+ window.location.replace(window.location.href);
+ return;
}
-
- window.newDrafts = [];
- window.deletedIds = [];
-
- window.location.replace(window.location.href);
- return;
- } else {
- toastMessage(`Unable to save entry: ${reply.error}`, 'danger');
}
} catch (err) {
toastMessage(`Network error: ${String(err)}`, 'danger');