From 8961609ae593095b4333cc0b39ae7cfe20139ac4 Mon Sep 17 00:00:00 2001 From: Zac Yungblut <66346164+zacyungblut@users.noreply.github.com> Date: Sat, 7 Nov 2026 20:50:29 -0500 Subject: [PATCH 1/2] fix(tanstack-start): group repeated search params into arrays for custom admin views Refactor searchToRecord function to group query parameters into arrays for consistent structure. --- .../src/adapters/views/index.tsx | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/tanstack-start/src/adapters/views/index.tsx b/packages/tanstack-start/src/adapters/views/index.tsx index 26d53f5d84a..7399f9d09b5 100644 --- a/packages/tanstack-start/src/adapters/views/index.tsx +++ b/packages/tanstack-start/src/adapters/views/index.tsx @@ -14,8 +14,26 @@ export type AdminLoad = (opts: { signal?: AbortSignal }) => Promise -const searchToRecord = (searchStr: string): Record => - Object.fromEntries(new URLSearchParams(searchStr)) +// `searchStr` is qs-serialized (see `payloadStringifySearch`), so arrays arrive as +// indexed keys (`operation[0]=update&operation[1]=delete`) and repeated plain keys +// stay repeated. Group both back into arrays so custom views receive the same +// `Record` shape the Next.js adapter passes. +const searchToRecord = (searchStr: string): Record => { + const search: Record = {} + + for (const [key, value] of new URLSearchParams(searchStr)) { + const baseKey = /^(.+)\[\d+\]$/.exec(key)?.[1] ?? key + const existing = search[baseKey] + + if (existing === undefined) { + search[baseKey] = value + } else { + search[baseKey] = Array.isArray(existing) ? [...existing, value] : [existing, value] + } + } + + return search +} const runLoader = async ({ load, From d36b2d21a740121b4c1b17024700e3d6dd81c85d Mon Sep 17 00:00:00 2001 From: Zac Yungblut <66346164+zacyungblut@users.noreply.github.com> Date: Sun, 8 Nov 2026 20:48:56 -0500 Subject: [PATCH 2/2] fix(tanstack-start): limit search param index grouping to top-level keys --- packages/tanstack-start/src/adapters/views/index.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/tanstack-start/src/adapters/views/index.tsx b/packages/tanstack-start/src/adapters/views/index.tsx index 7399f9d09b5..46e34b5c9d3 100644 --- a/packages/tanstack-start/src/adapters/views/index.tsx +++ b/packages/tanstack-start/src/adapters/views/index.tsx @@ -14,15 +14,16 @@ export type AdminLoad = (opts: { signal?: AbortSignal }) => Promise -// `searchStr` is qs-serialized (see `payloadStringifySearch`), so arrays arrive as -// indexed keys (`operation[0]=update&operation[1]=delete`) and repeated plain keys -// stay repeated. Group both back into arrays so custom views receive the same -// `Record` shape the Next.js adapter passes. +// `searchStr` is qs-serialized (see `payloadStringifySearch`), so top-level arrays +// arrive as indexed keys (`operation[0]=update&operation[1]=delete`) and repeated +// plain keys stay repeated. Group both back into arrays so custom views receive the +// same `Record` shape the Next.js adapter passes. +// Nested bracket keys (`filter[op]`, `filter[op][0]`) stay flat, as they do on Next.js. const searchToRecord = (searchStr: string): Record => { const search: Record = {} for (const [key, value] of new URLSearchParams(searchStr)) { - const baseKey = /^(.+)\[\d+\]$/.exec(key)?.[1] ?? key + const baseKey = /^([^\[\]]+)\[\d+\]$/.exec(key)?.[1] ?? key const existing = search[baseKey] if (existing === undefined) {