fix(admin): keep the filter query intact in console pager and export links
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m6s

The paginated users and messages lists interpolate the pre-encoded filter
query (url.Values.Encode) after the "?" in their pager and CSV-export links.
There html/template treats it as a single query value and percent-encodes the
structural "=" and "&" again, so "kind=robots" rendered as "kind%3drobots" and
the multi-pair message filter collapsed -- every page step dropped the active
filter.

Type FilterQuery as template.URL so the already-escaped fragment is emitted
verbatim (the attribute-level "&" -> "&" stays correct, the browser decodes
it back). It is safe because url.Values.Encode output is strictly
percent-encoded. games/complaints use status={{.Status}} -- a single value in
proper query-value context -- and were never affected.
This commit is contained in:
Ilia Denisov
2026-06-12 11:38:26 +02:00
parent 641ac88b2d
commit eeb078d528
3 changed files with 63 additions and 6 deletions
@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/csv"
"fmt"
"html/template"
"net/http"
"net/url"
"path/filepath"
@@ -108,7 +109,7 @@ func (s *Server) consoleUsers(c *gin.Context) {
view := adminconsole.UsersView{
Pager: adminconsole.NewPager(page, adminPageSize, total),
Robots: filter.Robots, NameMask: filter.NameMask, ExternalIDMask: filter.ExternalIDMask,
FilterQuery: q.Encode(),
FilterQuery: template.URL(q.Encode()),
}
ids := make([]uuid.UUID, 0, len(items))
for _, it := range items {
@@ -174,7 +175,7 @@ func (s *Server) consoleMessages(c *gin.Context) {
Pager: adminconsole.NewPager(page, adminPageSize, total),
NameMask: filter.NameMask,
ExtMask: filter.ExtMask,
FilterQuery: q.Encode(),
FilterQuery: template.URL(q.Encode()),
}
if filter.GameID != uuid.Nil {
view.GameID = filter.GameID.String()