Skip to content

Commit 4d4aae3

Browse files
committed
fix(ui): clear stale search params when input is emptied during pagination
When the user clears the search input and paginates before the debounced reload fires (~250ms window), stale q/tags values from data-extra-params would persist in the pagination request. Fix by calling url.searchParams.delete() when the input exists but is empty, ensuring the live DOM state is always authoritative over server-rendered extras. Add 9 tests covering the dynamic input-reading logic in pagination_controls.html: override, clear, trim, and tableName guard. Refs: #3128 Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
1 parent 3026134 commit 4d4aae3

File tree

2 files changed

+152
-6
lines changed

2 files changed

+152
-6
lines changed

mcpgateway/templates/pagination_controls.html

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,25 @@
112112
if (this.tableName) {
113113
const searchInputId = this.tableName + '-search-input';
114114
const searchInput = document.getElementById(searchInputId);
115-
if (searchInput && searchInput.value.trim()) {
116-
url.searchParams.set('q', searchInput.value.trim());
115+
if (searchInput) {
116+
const trimmedQuery = searchInput.value.trim();
117+
if (trimmedQuery) {
118+
url.searchParams.set('q', trimmedQuery);
119+
} else {
120+
url.searchParams.delete('q');
121+
}
117122
}
118-
123+
119124
// Also preserve tag filter if present
120125
const tagInputId = this.tableName + '-tag-filter';
121126
const tagInput = document.getElementById(tagInputId);
122-
if (tagInput && tagInput.value.trim()) {
123-
url.searchParams.set('tags', tagInput.value.trim());
127+
if (tagInput) {
128+
const trimmedTags = tagInput.value.trim();
129+
if (trimmedTags) {
130+
url.searchParams.set('tags', trimmedTags);
131+
} else {
132+
url.searchParams.delete('tags');
133+
}
124134
}
125135
}
126136

tests/js/admin-pagination.test.js

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,142 @@ describe("pagination_controls data-extra-params handling", () => {
772772
});
773773
});
774774

775+
// ---------------------------------------------------------------------------
776+
// pagination_controls: dynamic search input reading (#3128)
777+
//
778+
// When paginating, loadPage() reads the current search and tag filter input
779+
// values from the DOM. This ensures the user's active search filter is
780+
// preserved across pages even if the input changed after the last server
781+
// render (which would make data-extra-params stale).
782+
// ---------------------------------------------------------------------------
783+
describe("pagination_controls dynamic search input reading (#3128)", () => {
784+
/**
785+
* Replicates the full loadPage() URL-building logic from
786+
* pagination_controls.html, including extraParams AND the dynamic
787+
* input-reading block added in #3128.
788+
*/
789+
function buildUrlWithInputs(
790+
baseUrl,
791+
extraParamsJson,
792+
tableName,
793+
inputs = {},
794+
) {
795+
const url = new URL(baseUrl, "http://localhost");
796+
url.searchParams.set("page", "1");
797+
url.searchParams.set("per_page", "50");
798+
799+
// Step 1: extraParams from server-rendered data attribute
800+
const extraParams = JSON.parse(extraParamsJson || "{}");
801+
Object.entries(extraParams).forEach(([k, v]) => {
802+
if (k !== "include_inactive" && v !== null && v !== undefined) {
803+
url.searchParams.set(k, String(v));
804+
}
805+
});
806+
807+
// Step 2: dynamic input reading (mirrors #3128 fix)
808+
if (tableName) {
809+
if (inputs.search !== undefined) {
810+
const trimmedQuery = inputs.search.trim();
811+
if (trimmedQuery) {
812+
url.searchParams.set("q", trimmedQuery);
813+
} else {
814+
url.searchParams.delete("q");
815+
}
816+
}
817+
if (inputs.tags !== undefined) {
818+
const trimmedTags = inputs.tags.trim();
819+
if (trimmedTags) {
820+
url.searchParams.set("tags", trimmedTags);
821+
} else {
822+
url.searchParams.delete("tags");
823+
}
824+
}
825+
}
826+
827+
return url;
828+
}
829+
830+
test("search input value is used for q param", () => {
831+
const url = buildUrlWithInputs("/admin/tools/partial", "{}", "tools", {
832+
search: "my query",
833+
});
834+
expect(url.searchParams.get("q")).toBe("my query");
835+
});
836+
837+
test("tag input value is used for tags param", () => {
838+
const url = buildUrlWithInputs("/admin/tools/partial", "{}", "tools", {
839+
tags: "prod,staging",
840+
});
841+
expect(url.searchParams.get("tags")).toBe("prod,staging");
842+
});
843+
844+
test("input values override stale extraParams q and tags", () => {
845+
const json = JSON.stringify({ q: "old query", tags: "old-tag" });
846+
const url = buildUrlWithInputs("/admin/tools/partial", json, "tools", {
847+
search: "new query",
848+
tags: "new-tag",
849+
});
850+
expect(url.searchParams.get("q")).toBe("new query");
851+
expect(url.searchParams.get("tags")).toBe("new-tag");
852+
});
853+
854+
test("empty input clears stale extraParams q", () => {
855+
const json = JSON.stringify({ q: "stale search" });
856+
const url = buildUrlWithInputs("/admin/tools/partial", json, "tools", {
857+
search: "",
858+
});
859+
expect(url.searchParams.has("q")).toBe(false);
860+
});
861+
862+
test("empty input clears stale extraParams tags", () => {
863+
const json = JSON.stringify({ tags: "stale-tag" });
864+
const url = buildUrlWithInputs("/admin/tools/partial", json, "tools", {
865+
tags: "",
866+
});
867+
expect(url.searchParams.has("tags")).toBe(false);
868+
});
869+
870+
test("whitespace-only input clears q", () => {
871+
const json = JSON.stringify({ q: "stale" });
872+
const url = buildUrlWithInputs("/admin/tools/partial", json, "tools", {
873+
search: " ",
874+
});
875+
expect(url.searchParams.has("q")).toBe(false);
876+
});
877+
878+
test("input values are trimmed", () => {
879+
const url = buildUrlWithInputs("/admin/tools/partial", "{}", "tools", {
880+
search: " hello ",
881+
tags: " alpha ",
882+
});
883+
expect(url.searchParams.get("q")).toBe("hello");
884+
expect(url.searchParams.get("tags")).toBe("alpha");
885+
});
886+
887+
test("other extraParams are preserved when input overrides q", () => {
888+
const json = JSON.stringify({
889+
q: "old",
890+
gateway_id: "42",
891+
team_id: "t1",
892+
});
893+
const url = buildUrlWithInputs("/admin/tools/partial", json, "tools", {
894+
search: "new",
895+
});
896+
expect(url.searchParams.get("q")).toBe("new");
897+
expect(url.searchParams.get("gateway_id")).toBe("42");
898+
expect(url.searchParams.get("team_id")).toBe("t1");
899+
});
900+
901+
test("skips input reading when tableName is empty", () => {
902+
const json = JSON.stringify({ q: "from-server" });
903+
const url = buildUrlWithInputs("/admin/tools/partial", json, "", {
904+
search: "from-input",
905+
});
906+
// Without tableName, input reading is skipped; extraParams value stands
907+
expect(url.searchParams.get("q")).toBe("from-server");
908+
});
909+
});
910+
775911
// ---------------------------------------------------------------------------
776912
// Pagination swapStyle used by loadPage (#3396)
777913
//
@@ -800,7 +936,7 @@ describe("pagination loadPage swapStyle (#3396)", () => {
800936
hasNext: true,
801937
hasPrev: false,
802938
targetSelector: "#tools-table",
803-
swapStyle: swapStyle,
939+
swapStyle,
804940
tableName: "tools",
805941
baseUrl: "/admin/tools/partial",
806942
$el: {

0 commit comments

Comments
 (0)