fix: pagination returning duplicate results when timestamps: false (#13920)

### What
Fixes a bug where collection docs paginate incorrectly when `timestamps:
false` is set — the same docs were appearing across multiple pages.

### Why
The `find` query sanitizes the `sort` parameter.  

- With `timestamps: true`, it defaults to `createdAt`
- With `timestamps: false`, it falls back to `_id`

That logic is correct, but in `find.ts` we always passed `timestamps:
true`, ignoring the collection config. With the right sort applied,
pagination works as expected.

### How
`find.ts` now passes `collectionConfig.timestamps` to
`buildSortParam()`, ensuring the correct sort field is chosen.

---

Fixes #13888
This commit is contained in:
Jessica Rynkar
2025-09-24 14:44:41 +01:00
committed by GitHub
parent 96c66125dd
commit 062c1d7e89
5 changed files with 70 additions and 2 deletions

View File

@@ -54,7 +54,7 @@ export const find: Find = async function find(
locale,
sort: sortArg || collectionConfig.defaultSort,
sortAggregation,
timestamps: true,
timestamps: collectionConfig.timestamps || false,
})
}

View File

@@ -0,0 +1,14 @@
import type { CollectionConfig } from 'payload'
export const noTimestampsSlug = 'no-timestamps'
export const NoTimestampsCollection: CollectionConfig = {
slug: noTimestampsSlug,
timestamps: false,
fields: [
{
name: 'title',
type: 'text',
},
],
}

View File

@@ -21,6 +21,7 @@ import { CollectionHidden } from './collections/Hidden.js'
import { ListDrawer } from './collections/ListDrawer.js'
import { ListViewSelectAPI } from './collections/ListViewSelectAPI/index.js'
import { CollectionNoApiView } from './collections/NoApiView.js'
import { NoTimestampsCollection } from './collections/NoTimestamps.js'
import { CollectionNotInView } from './collections/NotInView.js'
import { Placeholder } from './collections/Placeholder.js'
import { Posts } from './collections/Posts.js'
@@ -191,6 +192,7 @@ export default buildConfigWithDefaults({
CustomListDrawer,
ListViewSelectAPI,
Virtuals,
NoTimestampsCollection,
],
globals: [
GlobalHidden,

View File

@@ -35,8 +35,8 @@ const description = 'Description'
let payload: PayloadTestSDK<Config>
import { listViewSelectAPISlug } from 'admin/collections/ListViewSelectAPI/index.js'
import { noTimestampsSlug } from 'admin/collections/NoTimestamps.js'
import { devUser } from 'credentials.js'
import { getRowByCellValueAndAssert } from 'helpers/e2e/getRowByCellValueAndAssert.js'
import {
openListColumns,
reorderColumns,
@@ -45,6 +45,7 @@ import {
waitForColumnInURL,
} from 'helpers/e2e/columns/index.js'
import { addListFilter, openListFilters } from 'helpers/e2e/filters/index.js'
import { getRowByCellValueAndAssert } from 'helpers/e2e/getRowByCellValueAndAssert.js'
import { goToNextPage, goToPreviousPage } from 'helpers/e2e/goToNextPage.js'
import { goToFirstCell } from 'helpers/e2e/navigateToDoc.js'
import { deletePreferences } from 'helpers/e2e/preferences.js'
@@ -76,6 +77,7 @@ describe('List View', () => {
let disableBulkEditUrl: AdminUrlUtil
let user: any
let virtualsUrl: AdminUrlUtil
let noTimestampsUrl: AdminUrlUtil
let serverURL: string
let adminRoutes: ReturnType<typeof getRoutes>
@@ -101,6 +103,7 @@ describe('List View', () => {
placeholderUrl = new AdminUrlUtil(serverURL, placeholderCollectionSlug)
disableBulkEditUrl = new AdminUrlUtil(serverURL, 'disable-bulk-edit')
virtualsUrl = new AdminUrlUtil(serverURL, virtualsSlug)
noTimestampsUrl = new AdminUrlUtil(serverURL, noTimestampsSlug)
const context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
@@ -1568,6 +1571,24 @@ describe('List View', () => {
await expect(page.locator('.per-page')).toContainText('Per Page: 15') // ensure this hasn't changed
await expect(page.locator('.page-controls__page-info')).toHaveText('16-16 of 16')
})
test('should paginate when timestamps are disabled', async () => {
await mapAsync([...Array(6)], async () => {
await createNoTimestampPost()
})
await page.goto(noTimestampsUrl.list)
await page.locator('.per-page .popup-button').click()
await page.getByRole('button', { name: '5', exact: true }).click()
await page.waitForURL(/limit=5/)
const firstPageIds = await page.locator('.cell-id').allInnerTexts()
await goToNextPage(page)
const secondPageIds = await page.locator('.cell-id').allInnerTexts()
expect(firstPageIds).not.toContain(secondPageIds[0])
})
})
// TODO: Troubleshoot flaky suite
@@ -1918,6 +1939,16 @@ async function createGeo(overrides?: Partial<Geo>): Promise<Geo> {
}) as unknown as Promise<Geo>
}
async function createNoTimestampPost(overrides?: Partial<Post>): Promise<Post> {
return payload.create({
collection: noTimestampsSlug,
data: {
title,
...overrides,
},
}) as unknown as Promise<Post>
}
async function createArray() {
return payload.create({
collection: arrayCollectionSlug,

View File

@@ -96,6 +96,7 @@ export interface Config {
'custom-list-drawer': CustomListDrawer;
'list-view-select-api': ListViewSelectApi;
virtuals: Virtual;
'no-timestamps': NoTimestamp;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
@@ -131,6 +132,7 @@ export interface Config {
'custom-list-drawer': CustomListDrawerSelect<false> | CustomListDrawerSelect<true>;
'list-view-select-api': ListViewSelectApiSelect<false> | ListViewSelectApiSelect<true>;
virtuals: VirtualsSelect<false> | VirtualsSelect<true>;
'no-timestamps': NoTimestampsSelect<false> | NoTimestampsSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
@@ -612,6 +614,14 @@ export interface Virtual {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "no-timestamps".
*/
export interface NoTimestamp {
id: string;
title?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
@@ -734,6 +744,10 @@ export interface PayloadLockedDocument {
| ({
relationTo: 'virtuals';
value: string | Virtual;
} | null)
| ({
relationTo: 'no-timestamps';
value: string | NoTimestamp;
} | null);
globalSlug?: string | null;
user: {
@@ -1175,6 +1189,13 @@ export interface VirtualsSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "no-timestamps_select".
*/
export interface NoTimestampsSelect<T extends boolean = true> {
title?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".