Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/engine/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,12 @@ const DEFAULT_ABSTRACT_QUERY_OPTIONS: AbstractQueryOptions = {
convertToQuery: (abstractQuery) => {
const query: Query = {}
for (const [key, value] of Object.entries(abstractQuery)) {
if (value !== undefined) {
query[key] = value.toString()
if (Array.isArray(value)) {
query[key] = value.map((v) => v.toString())
} else {
if (value !== undefined) {
query[key] = value.toString()
}
}
}
return query
Expand Down
22 changes: 22 additions & 0 deletions src/routers/__test__/inMemory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,26 @@ describe('InMemory', () => {

expect(result.current.query.search).toBe(search)
})

test('should handle array query parameters with single and multiple options', () => {
const optionsSchema = z.object({
options: z.array(z.string()),
})

const { result } = renderHook(() =>
useQuery(optionsSchema, { options: [] }),
)

act(() => {
result.current.setQuery({ options: ['A'] })
})

expect(result.current.query.options).toStrictEqual(['A'])

act(() => {
result.current.setQuery({ options: ['A', 'B'] })
})

expect(result.current.query.options).toStrictEqual(['A', 'B'])
})
})
63 changes: 59 additions & 4 deletions src/routers/__test__/nextRouter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe('NextRouter', () => {

const searchSchema = z.object({
search: z.string().optional().catch(undefined),
options: z.string().or(z.array(z.string())).optional().catch(undefined),
})

const useNextRouterQueryWithSearch = (
Expand Down Expand Up @@ -289,20 +290,50 @@ describe('NextRouter', () => {
expect(router.query.greeting).toBe(greeting)
})

test('query keys with default value should not be stored in the url', () => {
const defaultSearch = ''
test('query keys with default value by reset should not be stored in the url', () => {
const defaultSearch = 'Default'
const defaultOptions = ['A', 'B']

router.query = { search: 'Max' }
router.query = { search: 'Max', options: ['X', 'Y'] }

const { result } = renderHook(() =>
useQuery(searchSchema, { search: defaultSearch }),
useQuery(searchSchema, {
search: defaultSearch,
options: defaultOptions,
}),
)

act(() => {
result.current.resetQuery()
})

expect(result.current.query.search).toBe(defaultSearch)
expect(result.current.query.options).toStrictEqual(defaultOptions)
expect(router.query.search).toBeUndefined()
})

test('query keys with default value by set should not be stored in the url', () => {
const defaultSearch = 'Default'
const defaultOptions = ['A', 'B']

router.query = { search: 'Max', options: ['X', 'Y'] }

const { result } = renderHook(() =>
useQuery(searchSchema, {
search: defaultSearch,
options: defaultOptions,
}),
)

act(() => {
result.current.setQuery({
search: defaultSearch,
options: defaultOptions,
})
})

expect(result.current.query.search).toBe(defaultSearch)
expect(result.current.query.options).toStrictEqual(defaultOptions)
expect(router.query.search).toBeUndefined()
})

Expand Down Expand Up @@ -345,4 +376,28 @@ describe('NextRouter', () => {
expect(result.current.query.department).toBeUndefined()
expect(result.current.query.role).toBe(defaultRole)
})

test('should handle array query parameters with single and multiple options', () => {
const optionsSchema = z.object({
options: z.array(z.string()),
})

const { result } = renderHook(() =>
useQuery(optionsSchema, { options: [] }),
)

act(() => {
result.current.setQuery({ options: ['A'] })
})

expect(result.current.query.options).toStrictEqual(['A'])
expect(router.query.options).toStrictEqual(['A'])

act(() => {
result.current.setQuery({ options: ['A', 'B'] })
})

expect(result.current.query.options).toStrictEqual(['A', 'B'])
expect(router.query.options).toStrictEqual(['A', 'B'])
})
})
63 changes: 59 additions & 4 deletions src/routers/__test__/reactRouter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe('ReactRouter', () => {

const searchSchema = z.object({
search: z.string().optional().catch(undefined),
options: z.string().or(z.array(z.string())).optional().catch(undefined),
})

const useReactRouterQueryWithSearch = (
Expand Down Expand Up @@ -225,20 +226,50 @@ describe('ReactRouter', () => {
)
})

test('query keys with default value should not be stored in the url', () => {
const defaultSearch = ''
test('query keys with default value by reset should not be stored in the url', () => {
const defaultSearch = 'Default'
const defaultOptions = ['A', 'B']

window.history.pushState({}, '', `/?search=Max`)
window.history.pushState({}, '', `/?search=Max&options=X&options=Y`)

const { result } = renderHookWithContext(() =>
useQuery(searchSchema, { search: defaultSearch }),
useQuery(searchSchema, {
search: defaultSearch,
options: defaultOptions,
}),
)

act(() => {
result.current.resetQuery()
})

expect(result.current.query.search).toBe(defaultSearch)
expect(result.current.query.options).toStrictEqual(defaultOptions)
expect(window.location.search).toBe('')
})

test('query keys with default value by set should not be stored in the url', () => {
const defaultSearch = 'Default'
const defaultOptions = ['A', 'B']

window.history.pushState({}, '', `/?search=Max&options=X&options=Y`)

const { result } = renderHookWithContext(() =>
useQuery(searchSchema, {
search: defaultSearch,
options: defaultOptions,
}),
)

act(() => {
result.current.setQuery({
search: defaultSearch,
options: defaultOptions,
})
})

expect(result.current.query.search).toBe(defaultSearch)
expect(result.current.query.options).toStrictEqual(defaultOptions)
expect(window.location.search).toBe('')
})

Expand All @@ -256,4 +287,28 @@ describe('ReactRouter', () => {
expect(result.current.query.search).toBe(search)
expect(window.location.search).toBe(`?search=`)
})

test('should handle array query parameters with single and multiple options', () => {
const optionsSchema = z.object({
options: z.string().or(z.array(z.string())),
})

const { result } = renderHookWithContext(() =>
useQuery(optionsSchema, { options: [] }),
)

act(() => {
result.current.setQuery({ options: ['A'] })
})

expect(result.current.query.options).toStrictEqual('A')
expect(window.location.search).toBe(`?options=A`)

act(() => {
result.current.setQuery({ options: ['A', 'B'] })
})

expect(result.current.query.options).toStrictEqual(['A', 'B'])
expect(window.location.search).toBe(`?options=A&options=B`)
})
})
43 changes: 23 additions & 20 deletions src/routers/reactRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
useAbstractQueryAndPagination,
Router,
} from '../engine'
import { QUERY_ARRAY_SEPARATOR, RouterWithHistoryOptions } from './shared'
import { RouterWithHistoryOptions } from './shared'

const DEFAULT_REACT_ROUTER_OPTIONS: RouterWithHistoryOptions = {
setQueryMethod: 'replace',
Expand All @@ -30,14 +30,9 @@ const useReactRouter = (
const getQuery = useCallback(
(defaultQuery: Query) => {
const query: Query = {}
for (const [key, value] of new URLSearchParams(search).entries()) {
const decodedValues = value
.split(QUERY_ARRAY_SEPARATOR)
.map((v) => decodeURIComponent(v))
Comment thread
lukasvice marked this conversation as resolved.
query[key] =
decodedValues.length === 1
? (decodedValues[0] as string)
: decodedValues
for (const key of new URLSearchParams(search).keys()) {
const value = new URLSearchParams(search).getAll(key)
query[key] = value.length === 1 ? (value[0] as string) : value
}
return { ...defaultQuery, ...query }
},
Expand All @@ -48,19 +43,27 @@ const useReactRouter = (
(query: Partial<Query>, defaultQuery: Query) => {
const urlSearchParams = new URLSearchParams(search)
for (const [key, value] of Object.entries(query)) {
if (value === defaultQuery[key]) {
urlSearchParams.delete(key)
} else if (value !== undefined) {
let values: string[]
if (Array.isArray(value)) {
values = value
if (value !== undefined) {
const defaultValue = defaultQuery[key]

if (
defaultValue !== undefined &&
value.toString() === defaultValue.toString()
) {
urlSearchParams.delete(key)
} else {
values = [value]
const [firstValue, ...restValues] = Array.isArray(value)
? value
: [value]

if (firstValue !== undefined) {
urlSearchParams.set(key, firstValue)
}

for (const restValue of restValues) {
urlSearchParams.append(key, restValue)
}
}
const encodedValues = values
.map((v) => encodeURIComponent(v))
.join(QUERY_ARRAY_SEPARATOR)
urlSearchParams.set(key, encodedValues)
}
}
navigate(
Expand Down
2 changes: 0 additions & 2 deletions src/routers/shared.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
export const QUERY_ARRAY_SEPARATOR = ','

export type RouterWithHistoryOptions = {
/**
* Whether the router should push a new URL to the browser history or replace the current URL.
Expand Down