Skip to content

Commit fad4ee6

Browse files
authored
fix(ui): pagination resets perPage (#10199)
When using various controls within the List View, those selections are sometimes not persisted. This is especially evident when selecting `perPage` from the List View, where the URL and UI would reflect this selection, but the controls would be stale. Similarly, after changing `perPage` then navigating to another page through the pagination controls, `perPage` would reset back to the original value. Same with the sort controls, where sorting by a particular column would not be reflected in the UI. This was because although we modify the URL search params and fire off a new query with those changes, we were not updating local component state.
1 parent 6b4842d commit fad4ee6

File tree

4 files changed

+73
-86
lines changed

4 files changed

+73
-86
lines changed

packages/ui/src/providers/ListQuery/index.tsx

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,17 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
6363

6464
const { onQueryChange } = useListDrawerContext()
6565

66-
const [currentQuery, setCurrentQuery] = useState(() => {
66+
const [currentQuery, setCurrentQuery] = useState<ListQuery>(() => {
6767
if (modifySearchParams) {
6868
return searchParams
6969
} else {
7070
return {}
7171
}
7272
})
7373

74+
const currentQueryRef = React.useRef(currentQuery)
75+
76+
// If the search params change externally, update the current query
7477
useEffect(() => {
7578
if (modifySearchParams) {
7679
setCurrentQuery(searchParams)
@@ -79,10 +82,10 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
7982

8083
const refineListData = useCallback(
8184
async (query: ListQuery) => {
82-
let pageQuery = 'page' in query ? query.page : currentQuery?.page
85+
let page = 'page' in query ? query.page : currentQuery?.page
8386

8487
if ('where' in query || 'search' in query) {
85-
pageQuery = '1'
88+
page = '1'
8689
}
8790

8891
const updatedPreferences: Record<string, unknown> = {}
@@ -103,14 +106,11 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
103106
}
104107

105108
const newQuery: ListQuery = {
106-
limit:
107-
'limit' in query
108-
? query.limit
109-
: ((currentQuery?.limit as string) ?? String(defaultLimit)),
110-
page: pageQuery as string,
111-
search: 'search' in query ? query.search : (currentQuery?.search as string),
109+
limit: 'limit' in query ? query.limit : (currentQuery?.limit ?? String(defaultLimit)),
110+
page,
111+
search: 'search' in query ? query.search : currentQuery?.search,
112112
sort: 'sort' in query ? query.sort : ((currentQuery?.sort as string) ?? defaultSort),
113-
where: 'where' in query ? query.where : (currentQuery?.where as Where),
113+
where: 'where' in query ? query.where : currentQuery?.where,
114114
}
115115

116116
if (modifySearchParams) {
@@ -122,6 +122,8 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
122122
const onChangeFn = onQueryChange || onQueryChangeFromProps
123123
onChangeFn(newQuery)
124124
}
125+
126+
setCurrentQuery(newQuery)
125127
},
126128
[
127129
currentQuery?.page,
@@ -176,27 +178,30 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
176178
[refineListData],
177179
)
178180

181+
// If `defaultLimit` or `defaultSort` are updated externally, update the query
179182
useEffect(() => {
180183
if (modifySearchParams) {
181184
let shouldUpdateQueryString = false
185+
const newQuery = { ...(currentQueryRef.current || {}) }
182186

183-
if (isNumber(defaultLimit) && !('limit' in currentQuery)) {
184-
currentQuery.limit = String(defaultLimit)
187+
// Allow the URL to override the default limit
188+
if (isNumber(defaultLimit) && !('limit' in currentQueryRef.current)) {
189+
newQuery.limit = String(defaultLimit)
185190
shouldUpdateQueryString = true
186191
}
187192

188-
if (defaultSort && !('sort' in currentQuery)) {
189-
currentQuery.sort = defaultSort
193+
// Allow the URL to override the default sort
194+
if (defaultSort && !('sort' in currentQueryRef.current)) {
195+
newQuery.sort = defaultSort
190196
shouldUpdateQueryString = true
191197
}
192198

193-
setCurrentQuery(currentQuery)
194-
195199
if (shouldUpdateQueryString) {
196-
router.replace(`?${qs.stringify(currentQuery)}`)
200+
setCurrentQuery(newQuery)
201+
router.replace(`?${qs.stringify(newQuery)}`)
197202
}
198203
}
199-
}, [defaultSort, defaultLimit, router, modifySearchParams, currentQuery])
204+
}, [defaultSort, defaultLimit, router, modifySearchParams])
200205

201206
return (
202207
<Context.Provider

packages/ui/src/views/List/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export const DefaultListView: React.FC<ListViewClientProps> = (props) => {
109109
handlePerPageChange,
110110
query,
111111
} = useListQuery()
112+
112113
const { openModal } = useModal()
113114
const { setCollectionSlug, setCurrentActivePath, setOnSuccess } = useBulkUpload()
114115
const { drawerSlug: bulkUploadDrawerSlug } = useBulkUpload()

test/admin/e2e/2/e2e.spec.ts

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -439,27 +439,25 @@ describe('admin2', () => {
439439

440440
test('should reset page when filters are applied', async () => {
441441
await deleteAllPosts()
442-
await mapAsync([...Array(6)], async () => {
443-
await createPost()
444-
})
445-
await page.reload()
446-
await mapAsync([...Array(6)], async () => {
447-
await createPost({ title: 'test' })
448-
})
442+
443+
await Promise.all(
444+
Array.from({ length: 12 }, async (_, i) => {
445+
if (i < 6) {
446+
await createPost()
447+
} else {
448+
await createPost({ title: 'test' })
449+
}
450+
}),
451+
)
452+
449453
await page.reload()
450454

451-
const pageInfo = page.locator('.collection-list__page-info')
452-
const perPage = page.locator('.per-page')
453455
const tableItems = page.locator(tableRowLocator)
454456

455457
await expect(tableItems).toHaveCount(10)
456-
await expect(pageInfo).toHaveText('1-10 of 12')
457-
await expect(perPage).toContainText('Per Page: 10')
458-
459-
// go to page 2
458+
await expect(page.locator('.collection-list__page-info')).toHaveText('1-10 of 12')
459+
await expect(page.locator('.per-page')).toContainText('Per Page: 10')
460460
await page.goto(`${postsUrl.list}?limit=10&page=2`)
461-
462-
// add filter
463461
await openListFilters(page, {})
464462
await page.locator('.where-builder__add-first-filter').click()
465463
await page.locator('.condition__field .rs__control').click()
@@ -468,9 +466,7 @@ describe('admin2', () => {
468466
await page.locator('.condition__operator .rs__control').click()
469467
await options.locator('text=equals').click()
470468
await page.locator('.condition__value input').fill('test')
471-
472-
// expect to be on page 1
473-
await expect(pageInfo).toHaveText('1-6 of 6')
469+
await expect(page.locator('.collection-list__page-info')).toHaveText('1-6 of 6')
474470
})
475471
})
476472

@@ -685,28 +681,52 @@ describe('admin2', () => {
685681
describe('pagination', () => {
686682
test('should paginate', async () => {
687683
await deleteAllPosts()
684+
688685
await mapAsync([...Array(11)], async () => {
689686
await createPost()
690687
})
691-
await page.reload()
692688

693-
const pageInfo = page.locator('.collection-list__page-info')
694-
const perPage = page.locator('.per-page')
695-
const paginator = page.locator('.paginator')
689+
await page.reload()
696690
const tableItems = page.locator(tableRowLocator)
697-
698691
await expect(tableItems).toHaveCount(10)
699-
await expect(pageInfo).toHaveText('1-10 of 11')
700-
await expect(perPage).toContainText('Per Page: 10')
701-
702-
// Forward one page and back using numbers
703-
await paginator.locator('button').nth(1).click()
692+
await expect(page.locator('.collection-list__page-info')).toHaveText('1-10 of 11')
693+
await expect(page.locator('.per-page')).toContainText('Per Page: 10')
694+
await page.locator('.paginator button').nth(1).click()
704695
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('page=2')
705696
await expect(tableItems).toHaveCount(1)
706-
await paginator.locator('button').nth(0).click()
697+
await page.locator('.paginator button').nth(0).click()
707698
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('page=1')
708699
await expect(tableItems).toHaveCount(10)
709700
})
701+
702+
test('should paginate and maintain perPage', async () => {
703+
await deleteAllPosts()
704+
705+
await mapAsync([...Array(26)], async () => {
706+
await createPost()
707+
})
708+
709+
await page.reload()
710+
const tableItems = page.locator(tableRowLocator)
711+
await expect(tableItems).toHaveCount(10)
712+
await expect(page.locator('.collection-list__page-info')).toHaveText('1-10 of 26')
713+
await expect(page.locator('.per-page')).toContainText('Per Page: 10')
714+
await page.locator('.per-page .popup-button').click()
715+
716+
await page
717+
.locator('.per-page button.per-page__button', {
718+
hasText: '25',
719+
})
720+
.click()
721+
722+
await expect(tableItems).toHaveCount(25)
723+
await expect(page.locator('.per-page .per-page__base-button')).toContainText('Per Page: 25')
724+
await page.locator('.paginator button').nth(1).click()
725+
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('page=2')
726+
await expect(tableItems).toHaveCount(1)
727+
await expect(page.locator('.per-page')).toContainText('Per Page: 25')
728+
await expect(page.locator('.collection-list__page-info')).toHaveText('26-26 of 26')
729+
})
710730
})
711731

712732
// TODO: Troubleshoot flaky suite

test/admin/payload-types.ts

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,6 @@ export interface Upload {
125125
focalY?: number | null;
126126
}
127127
/**
128-
* This is a custom collection description.
129-
*
130128
* This interface was referenced by `Config`'s JSON-Schema
131129
* via the `definition` "posts".
132130
*/
@@ -167,9 +165,6 @@ export interface Post {
167165
defaultValueField?: string | null;
168166
relationship?: (string | null) | Post;
169167
customCell?: string | null;
170-
/**
171-
* This is a very long description that takes many characters to complete and hopefully will wrap instead of push the sidebar open, lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, voluptatum voluptates. Quisquam, voluptatum voluptates.
172-
*/
173168
sidebarField?: string | null;
174169
updatedAt: string;
175170
createdAt: string;
@@ -251,13 +246,7 @@ export interface CustomField {
251246
id: string;
252247
customTextServerField?: string | null;
253248
customTextClientField?: string | null;
254-
/**
255-
* Static field description.
256-
*/
257249
descriptionAsString?: string | null;
258-
/**
259-
* Function description
260-
*/
261250
descriptionAsFunction?: string | null;
262251
descriptionAsComponent?: string | null;
263252
customSelectField?: string | null;
@@ -339,34 +328,6 @@ export interface Geo {
339328
updatedAt: string;
340329
createdAt: string;
341330
}
342-
/**
343-
* Description
344-
*
345-
* This interface was referenced by `Config`'s JSON-Schema
346-
* via the `definition` "customIdTab".
347-
*/
348-
export interface CustomIdTab {
349-
title?: string | null;
350-
id: string;
351-
description?: string | null;
352-
number?: number | null;
353-
updatedAt: string;
354-
createdAt: string;
355-
}
356-
/**
357-
* Description
358-
*
359-
* This interface was referenced by `Config`'s JSON-Schema
360-
* via the `definition` "customIdRow".
361-
*/
362-
export interface CustomIdRow {
363-
title?: string | null;
364-
id: string;
365-
description?: string | null;
366-
number?: number | null;
367-
updatedAt: string;
368-
createdAt: string;
369-
}
370331
/**
371332
* This interface was referenced by `Config`'s JSON-Schema
372333
* via the `definition` "disable-duplicate".

0 commit comments

Comments
 (0)