From c03c8478b0487b951a41d24dfbcb160cfaf3f8a7 Mon Sep 17 00:00:00 2001 From: IanM Date: Mon, 20 Apr 2026 22:53:13 +0100 Subject: [PATCH] fix: prevent DiscussionListState cache wipe on back-nav to tag pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DiscussionListState.requestParams returned `this.params.filter` by reference. Extenders (tags, subscriptions) then mutated that object to build the outgoing API filter, silently writing their additions back into the stored state. On the next mount, the freshly-supplied filter no longer equalled the mutated stored copy, so `paramsChanged()` tripped and the paginated cache was wiped — the visible "back button resets to page 1" symptom on tag pages (#4583). Clone `filter` before handing it out so extender mutation lands on the per-request object, not on `this.params`. --- .../src/forum/states/DiscussionListState.ts | 5 +++- .../forum/states/DiscussionListState.test.ts | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 framework/core/js/tests/unit/forum/states/DiscussionListState.test.ts diff --git a/framework/core/js/src/forum/states/DiscussionListState.ts b/framework/core/js/src/forum/states/DiscussionListState.ts index f53f2b7514..aab272961b 100644 --- a/framework/core/js/src/forum/states/DiscussionListState.ts +++ b/framework/core/js/src/forum/states/DiscussionListState.ts @@ -25,9 +25,12 @@ export default class DiscussionListState

bootstrapForum()); + +describe('DiscussionListState', () => { + describe('requestParams', () => { + // Regression test for #4583. + // + // Extenders (tags, subscriptions, …) mutate `params.filter` inside a + // `requestParams` extender callback. If `requestParams` returns + // `this.params.filter` by reference, those mutations leak back into the + // stored state — and on the next mount `paramsChanged()` falsely reports + // a change, wiping the paginated cache and resetting the list to page 1. + test('does not leak this.params.filter to callers', () => { + const state = new DiscussionListState({ filter: { tag: 'foo' } } as any); + + const out = state.requestParams() as { filter: Record }; + out.filter.injectedByExtender = 'yes'; + + expect((state as any).params.filter).toEqual({ tag: 'foo' }); + expect((state as any).params.filter.injectedByExtender).toBeUndefined(); + }); + }); +});