Skip to content

Commit 0aef187

Browse files
authored
fix(ui): preserve pagination page after edit modal save (#3389)
_navigateAdmin() now copies namespaced pagination params (*_page, *_size, *_inactive, *_q, *_tags) from the current URL into the outgoing searchParams — but only when the caller has not already explicitly set that key. Bare include_inactive is excluded (callers set it directly). Closes #3388 Signed-off-by: Shoumi <shoumimukherjee@gmail.com> Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
1 parent 3c970d5 commit 0aef187

File tree

2 files changed

+170
-2
lines changed

2 files changed

+170
-2
lines changed

mcpgateway/static/admin.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -861,7 +861,26 @@ function _navigateAdmin(fragment, searchParams) {
861861
adminIdx >= 0
862862
? window.location.origin + currentPath.slice(0, adminIdx)
863863
: window.ROOT_PATH || window.location.origin;
864-
const qs = searchParams ? searchParams.toString() : "";
864+
865+
// Preserve namespaced pagination state (*_page, *_size, *_inactive, *_q, *_tags)
866+
// from the current URL so that editing an item on page 3 returns to page 3.
867+
if (!searchParams) {
868+
searchParams = new URLSearchParams();
869+
}
870+
const currentUrlParams = new URLSearchParams(window.location.search);
871+
currentUrlParams.forEach((value, key) => {
872+
const isPaginationParam =
873+
key.endsWith("_page") ||
874+
key.endsWith("_size") ||
875+
(key.endsWith("_inactive") && key !== "include_inactive") ||
876+
key.endsWith("_q") ||
877+
key.endsWith("_tags");
878+
if (isPaginationParam && !searchParams.has(key)) {
879+
searchParams.set(key, value);
880+
}
881+
});
882+
883+
const qs = searchParams.toString();
865884
const target = `${base}/admin${qs ? `?${qs}` : ""}#${fragment}`;
866885

867886
// When the target URL is identical to the current URL (same path, query,

tests/js/admin-pagination.test.js

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@ beforeAll(() => {
7575
}
7676

7777
if (
78-
!additionalParams.hasOwnProperty("include_inactive") &&
78+
!Object.prototype.hasOwnProperty.call(
79+
additionalParams,
80+
"include_inactive",
81+
) &&
7982
params.includeInactive !== null
8083
) {
8184
url.searchParams.set(
@@ -848,3 +851,149 @@ describe("pagination loadPage swapStyle (#3396)", () => {
848851
expect(ajaxCalls[0].swap).toBe("innerHTML");
849852
});
850853
});
854+
855+
// ---------------------------------------------------------------------------
856+
// _navigateAdmin: pagination state preservation (#3389)
857+
//
858+
// _navigateAdmin is the single function all edit/save/toggle handlers use to
859+
// redirect after a successful operation. The fix copies namespaced pagination
860+
// params (*_page, *_size, *_inactive, *_q, *_tags) from the current URL into
861+
// the outgoing searchParams so editing an item on page 3 returns to page 3.
862+
//
863+
// Testing strategy: the function mutates the passed URLSearchParams before
864+
// attempting navigation (which throws "Not implemented" in JSDOM). We inspect
865+
// the URLSearchParams object after catching the error to verify the mutation.
866+
// ---------------------------------------------------------------------------
867+
describe("_navigateAdmin pagination state preservation (#3389)", () => {
868+
/**
869+
* Call _navigateAdmin and swallow the JSDOM navigation error.
870+
* Returns the searchParams object after mutation.
871+
*/
872+
function callNavigateAdmin(fragment, searchParams) {
873+
try {
874+
win._navigateAdmin(fragment, searchParams);
875+
} catch (_) {
876+
// JSDOM throws "Not implemented: navigation" — expected
877+
}
878+
return searchParams;
879+
}
880+
881+
test("preserves *_page and *_size params from current URL", () => {
882+
win.history.replaceState({}, "", "/admin?tools_page=3&tools_size=25");
883+
const params = new win.URLSearchParams();
884+
callNavigateAdmin("tools", params);
885+
expect(params.get("tools_page")).toBe("3");
886+
expect(params.get("tools_size")).toBe("25");
887+
});
888+
889+
test("preserves *_q and *_tags params from current URL", () => {
890+
win.history.replaceState(
891+
{},
892+
"",
893+
"/admin?tools_q=search&tools_tags=alpha,beta",
894+
);
895+
const params = new win.URLSearchParams();
896+
callNavigateAdmin("tools", params);
897+
expect(params.get("tools_q")).toBe("search");
898+
expect(params.get("tools_tags")).toBe("alpha,beta");
899+
});
900+
901+
test("preserves namespaced *_inactive params (e.g. tools_inactive)", () => {
902+
win.history.replaceState({}, "", "/admin?tools_inactive=true");
903+
const params = new win.URLSearchParams();
904+
callNavigateAdmin("tools", params);
905+
expect(params.get("tools_inactive")).toBe("true");
906+
});
907+
908+
test("does NOT preserve bare include_inactive param", () => {
909+
win.history.replaceState({}, "", "/admin?include_inactive=true");
910+
const params = new win.URLSearchParams();
911+
callNavigateAdmin("tools", params);
912+
expect(params.has("include_inactive")).toBe(false);
913+
});
914+
915+
test("does NOT preserve non-pagination params (e.g. team_id, random)", () => {
916+
win.history.replaceState(
917+
{},
918+
"",
919+
"/admin?team_id=abc&random=42&tools_page=2",
920+
);
921+
const params = new win.URLSearchParams();
922+
callNavigateAdmin("tools", params);
923+
expect(params.has("team_id")).toBe(false);
924+
expect(params.has("random")).toBe(false);
925+
expect(params.get("tools_page")).toBe("2");
926+
});
927+
928+
test("caller-set params take precedence over URL params", () => {
929+
win.history.replaceState({}, "", "/admin?tools_page=5&tools_size=50");
930+
const params = new win.URLSearchParams();
931+
params.set("tools_page", "1");
932+
callNavigateAdmin("tools", params);
933+
expect(params.get("tools_page")).toBe("1");
934+
expect(params.get("tools_size")).toBe("50");
935+
});
936+
937+
test("preserves params across multiple table namespaces", () => {
938+
win.history.replaceState(
939+
{},
940+
"",
941+
"/admin?tools_page=3&gateways_page=2&servers_size=50&agents_q=bot",
942+
);
943+
const params = new win.URLSearchParams();
944+
callNavigateAdmin("tools", params);
945+
expect(params.get("tools_page")).toBe("3");
946+
expect(params.get("gateways_page")).toBe("2");
947+
expect(params.get("servers_size")).toBe("50");
948+
expect(params.get("agents_q")).toBe("bot");
949+
});
950+
951+
test("handles null searchParams without TypeError", () => {
952+
win.history.replaceState({}, "", "/admin?tools_page=4");
953+
let typeError = false;
954+
try {
955+
win._navigateAdmin("tools", null);
956+
} catch (e) {
957+
if (e.message && e.message.includes("Cannot read properties")) {
958+
typeError = true;
959+
}
960+
}
961+
expect(typeError).toBe(false);
962+
});
963+
964+
test("handles undefined searchParams without TypeError", () => {
965+
win.history.replaceState({}, "", "/admin?tools_page=4");
966+
let typeError = false;
967+
try {
968+
win._navigateAdmin("tools");
969+
} catch (e) {
970+
if (e.message && e.message.includes("Cannot read properties")) {
971+
typeError = true;
972+
}
973+
}
974+
expect(typeError).toBe(false);
975+
});
976+
977+
test("preserves all five pagination suffixes simultaneously", () => {
978+
win.history.replaceState(
979+
{},
980+
"",
981+
"/admin?tools_page=3&tools_size=25&tools_inactive=true&tools_q=search&tools_tags=v1,v2",
982+
);
983+
const params = new win.URLSearchParams();
984+
callNavigateAdmin("tools", params);
985+
expect(params.get("tools_page")).toBe("3");
986+
expect(params.get("tools_size")).toBe("25");
987+
expect(params.get("tools_inactive")).toBe("true");
988+
expect(params.get("tools_q")).toBe("search");
989+
expect(params.get("tools_tags")).toBe("v1,v2");
990+
});
991+
992+
test("does not overwrite caller include_inactive with URL's bare include_inactive", () => {
993+
win.history.replaceState({}, "", "/admin?include_inactive=true");
994+
const params = new win.URLSearchParams();
995+
params.set("include_inactive", "false");
996+
callNavigateAdmin("tools", params);
997+
expect(params.get("include_inactive")).toBe("false");
998+
});
999+
});

0 commit comments

Comments
 (0)