diff --git a/resources/js/wayfinder.ts b/resources/js/wayfinder.ts index 8e826a2..c0a3ecb 100644 --- a/resources/js/wayfinder.ts +++ b/resources/js/wayfinder.ts @@ -1,13 +1,13 @@ -export type QueryParams = Record< - string, - | string - | number - | boolean - | string[] - | null - | undefined - | Record ->; +export type QueryParams = { + [key: string]: + | string + | number + | boolean + | (string | number)[] + | null + | undefined + | QueryParams; +}; type Method = "get" | "post" | "put" | "delete" | "patch" | "head" | "options"; @@ -74,22 +74,30 @@ export const queryParams = (options?: RouteQueryOptions) => { } }); - for (const subKey in query[key]) { - if (typeof query[key][subKey] === "undefined") { - continue; - } - - if ( - ["string", "number", "boolean"].includes( - typeof query[key][subKey], - ) - ) { - params.set( - `${key}[${subKey}]`, - getValue(query[key][subKey]), - ); - } - } + const addNestedParams = (obj: QueryParams, prefix: string) => { + Object.entries(obj).forEach(([subKey, value]) => { + if (value === undefined) return; + + const paramKey = `${prefix}[${subKey}]`; + + if (Array.isArray(value)) { + value.forEach((v) => + params.append(`${paramKey}[]`, getValue(v)), + ); + } else if (value !== null && typeof value === "object") { + addNestedParams(value, paramKey); + } else if ( + ["string", "number", "boolean"].includes(typeof value) + ) { + params.set( + paramKey, + getValue(value as string | number | boolean), + ); + } + }); + }; + + addNestedParams(query[key], key); } else { params.set(key, getValue(query[key])); } diff --git a/tests/QueryParams.test.ts b/tests/QueryParams.test.ts index 8e0a611..aa8f442 100644 --- a/tests/QueryParams.test.ts +++ b/tests/QueryParams.test.ts @@ -1,5 +1,8 @@ import { expect, it } from "vitest"; -import { index } from "../workbench/resources/js/actions/App/Http/Controllers/PostController"; +import { + index, + show, +} from "../workbench/resources/js/actions/App/Http/Controllers/PostController"; it("can convert basic params", () => { expect( @@ -169,6 +172,29 @@ it("can merge with the form method", () => { }); }); +it("can pass nested query parameters", () => { + expect( + show( + { post: 1 }, + { + query: { + search: "Hello World", + filters: { + users: ["james", "michael", "john"], + attributes: { + visible: true, + status: "example", + }, + }, + }, + }, + ), + ).toEqual({ + url: "/posts/1?search=Hello+World&filters%5Busers%5D%5B%5D=james&filters%5Busers%5D%5B%5D=michael&filters%5Busers%5D%5B%5D=john&filters%5Battributes%5D%5Bvisible%5D=1&filters%5Battributes%5D%5Bstatus%5D=example", + method: "get", + }); +}); + it("ignores nested object values with unallowed types", () => { window.location.search = "?parent=og"; @@ -178,17 +204,17 @@ it("ignores nested object values with unallowed types", () => { boolean: boolean; } => { const obj = { - string: 'string', + string: "string", number: 5, boolean: true, undefined: undefined, null: null, array: [], object: {}, - } + }; - return obj - } + return obj; + }; expect( index.form.head({