web/js/api-client.js wraps fetch with one failure path (ApiError for non-2xx,
success:false and non-JSON bodies) and latestOnly(), which aborts the previous
in-flight request and resolves superseded calls to STALE. The admin search and
recent-page loads share one guard, the gallery page load has its own, so fast
typing or clicking can no longer paint an older result over a newer one.
In-place prompt editing used a blur listener with {once:true} plus a keydown
listener added on every edit, which sent the PUT twice and stacked handlers.
The list now has one keydown/focusout pair bound at construction and
finishEdit() clears the edit state before touching the element, so a save is
sent exactly once. Mutations re-fetch the page currently shown (refreshList)
instead of calling search(), and delete, bulk delete, edit, restore and scan no
longer reload the whole page.
Pagination state is committed only after a successful fetch via the pure
ListState.nextPageState(current, response) in both admin and gallery, so a
failed or aborted page request leaves the current page intact. The gallery
lightbox removed its keydown handlers on a non-existent 'remove' event; a
MutationObserver now drops them when the modal leaves the document.
web/js/sse-stream.js reads server-sent events over fetch so POST-only progress
endpoints can stream.
51 lines
2.2 KiB
JavaScript
51 lines
2.2 KiB
JavaScript
/**
|
|
* Pure state helpers for the paginated prompt and image lists.
|
|
*
|
|
* Loadable in the browser (window.ListState) and in Node for tests.
|
|
*/
|
|
(function (root) {
|
|
"use strict";
|
|
|
|
function nonNegativeInt(value) {
|
|
return Number.isInteger(value) && value >= 0 ? value : null;
|
|
}
|
|
|
|
/**
|
|
* Pagination state after a page fetch succeeded. Callers keep their current
|
|
* state untouched while the request is in flight and only adopt the result.
|
|
*
|
|
* @param {{page: number, limit: number, total: number}} current
|
|
* @param {{page?: number, limit?: number, total?: number}} response - the page
|
|
* that was requested and the total the server reported
|
|
* @returns {{page: number, limit: number, total: number, totalPages: number}}
|
|
*/
|
|
function nextPageState(current, response) {
|
|
const limit = Number.isInteger(response.limit) && response.limit > 0 ? response.limit : current.limit;
|
|
const total = nonNegativeInt(response.total) ?? current.total;
|
|
const totalPages = Math.max(1, Math.ceil(total / limit));
|
|
const requested = Number.isInteger(response.page) ? response.page : current.page;
|
|
const page = Math.min(Math.max(1, requested), totalPages);
|
|
return { page, limit, total, totalPages };
|
|
}
|
|
|
|
/** Whether an in-place prompt edit changed the text to something worth saving. */
|
|
function shouldSaveEdit(originalText, newText) {
|
|
if (typeof newText !== "string") return false;
|
|
const trimmed = newText.trim();
|
|
return trimmed !== "" && trimmed !== String(originalText ?? "").trim();
|
|
}
|
|
|
|
/**
|
|
* Whether another page should be fetched from a capped list endpoint such as
|
|
* /prompt_manager/images/all: the server said has_more and the last page was not empty.
|
|
*/
|
|
function hasMorePages(response, receivedCount) {
|
|
const pagination = response && response.pagination;
|
|
return Boolean(pagination && pagination.has_more) && receivedCount > 0;
|
|
}
|
|
|
|
const api = { nextPageState, shouldSaveEdit, hasMorePages };
|
|
if (typeof module !== "undefined" && module.exports) module.exports = api;
|
|
else root.ListState = api;
|
|
})(typeof window !== "undefined" ? window : globalThis);
|