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:
14
test/admin/collections/NoTimestamps.ts
Normal file
14
test/admin/collections/NoTimestamps.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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".
|
||||
|
||||
Reference in New Issue
Block a user