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:
@@ -54,7 +54,7 @@ export const find: Find = async function find(
|
|||||||
locale,
|
locale,
|
||||||
sort: sortArg || collectionConfig.defaultSort,
|
sort: sortArg || collectionConfig.defaultSort,
|
||||||
sortAggregation,
|
sortAggregation,
|
||||||
timestamps: true,
|
timestamps: collectionConfig.timestamps || false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 { ListDrawer } from './collections/ListDrawer.js'
|
||||||
import { ListViewSelectAPI } from './collections/ListViewSelectAPI/index.js'
|
import { ListViewSelectAPI } from './collections/ListViewSelectAPI/index.js'
|
||||||
import { CollectionNoApiView } from './collections/NoApiView.js'
|
import { CollectionNoApiView } from './collections/NoApiView.js'
|
||||||
|
import { NoTimestampsCollection } from './collections/NoTimestamps.js'
|
||||||
import { CollectionNotInView } from './collections/NotInView.js'
|
import { CollectionNotInView } from './collections/NotInView.js'
|
||||||
import { Placeholder } from './collections/Placeholder.js'
|
import { Placeholder } from './collections/Placeholder.js'
|
||||||
import { Posts } from './collections/Posts.js'
|
import { Posts } from './collections/Posts.js'
|
||||||
@@ -191,6 +192,7 @@ export default buildConfigWithDefaults({
|
|||||||
CustomListDrawer,
|
CustomListDrawer,
|
||||||
ListViewSelectAPI,
|
ListViewSelectAPI,
|
||||||
Virtuals,
|
Virtuals,
|
||||||
|
NoTimestampsCollection,
|
||||||
],
|
],
|
||||||
globals: [
|
globals: [
|
||||||
GlobalHidden,
|
GlobalHidden,
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ const description = 'Description'
|
|||||||
let payload: PayloadTestSDK<Config>
|
let payload: PayloadTestSDK<Config>
|
||||||
|
|
||||||
import { listViewSelectAPISlug } from 'admin/collections/ListViewSelectAPI/index.js'
|
import { listViewSelectAPISlug } from 'admin/collections/ListViewSelectAPI/index.js'
|
||||||
|
import { noTimestampsSlug } from 'admin/collections/NoTimestamps.js'
|
||||||
import { devUser } from 'credentials.js'
|
import { devUser } from 'credentials.js'
|
||||||
import { getRowByCellValueAndAssert } from 'helpers/e2e/getRowByCellValueAndAssert.js'
|
|
||||||
import {
|
import {
|
||||||
openListColumns,
|
openListColumns,
|
||||||
reorderColumns,
|
reorderColumns,
|
||||||
@@ -45,6 +45,7 @@ import {
|
|||||||
waitForColumnInURL,
|
waitForColumnInURL,
|
||||||
} from 'helpers/e2e/columns/index.js'
|
} from 'helpers/e2e/columns/index.js'
|
||||||
import { addListFilter, openListFilters } from 'helpers/e2e/filters/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 { goToNextPage, goToPreviousPage } from 'helpers/e2e/goToNextPage.js'
|
||||||
import { goToFirstCell } from 'helpers/e2e/navigateToDoc.js'
|
import { goToFirstCell } from 'helpers/e2e/navigateToDoc.js'
|
||||||
import { deletePreferences } from 'helpers/e2e/preferences.js'
|
import { deletePreferences } from 'helpers/e2e/preferences.js'
|
||||||
@@ -76,6 +77,7 @@ describe('List View', () => {
|
|||||||
let disableBulkEditUrl: AdminUrlUtil
|
let disableBulkEditUrl: AdminUrlUtil
|
||||||
let user: any
|
let user: any
|
||||||
let virtualsUrl: AdminUrlUtil
|
let virtualsUrl: AdminUrlUtil
|
||||||
|
let noTimestampsUrl: AdminUrlUtil
|
||||||
|
|
||||||
let serverURL: string
|
let serverURL: string
|
||||||
let adminRoutes: ReturnType<typeof getRoutes>
|
let adminRoutes: ReturnType<typeof getRoutes>
|
||||||
@@ -101,6 +103,7 @@ describe('List View', () => {
|
|||||||
placeholderUrl = new AdminUrlUtil(serverURL, placeholderCollectionSlug)
|
placeholderUrl = new AdminUrlUtil(serverURL, placeholderCollectionSlug)
|
||||||
disableBulkEditUrl = new AdminUrlUtil(serverURL, 'disable-bulk-edit')
|
disableBulkEditUrl = new AdminUrlUtil(serverURL, 'disable-bulk-edit')
|
||||||
virtualsUrl = new AdminUrlUtil(serverURL, virtualsSlug)
|
virtualsUrl = new AdminUrlUtil(serverURL, virtualsSlug)
|
||||||
|
noTimestampsUrl = new AdminUrlUtil(serverURL, noTimestampsSlug)
|
||||||
const context = await browser.newContext()
|
const context = await browser.newContext()
|
||||||
page = await context.newPage()
|
page = await context.newPage()
|
||||||
initPageConsoleErrorCatch(page)
|
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('.per-page')).toContainText('Per Page: 15') // ensure this hasn't changed
|
||||||
await expect(page.locator('.page-controls__page-info')).toHaveText('16-16 of 16')
|
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
|
// TODO: Troubleshoot flaky suite
|
||||||
@@ -1918,6 +1939,16 @@ async function createGeo(overrides?: Partial<Geo>): Promise<Geo> {
|
|||||||
}) as unknown as 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() {
|
async function createArray() {
|
||||||
return payload.create({
|
return payload.create({
|
||||||
collection: arrayCollectionSlug,
|
collection: arrayCollectionSlug,
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export interface Config {
|
|||||||
'custom-list-drawer': CustomListDrawer;
|
'custom-list-drawer': CustomListDrawer;
|
||||||
'list-view-select-api': ListViewSelectApi;
|
'list-view-select-api': ListViewSelectApi;
|
||||||
virtuals: Virtual;
|
virtuals: Virtual;
|
||||||
|
'no-timestamps': NoTimestamp;
|
||||||
'payload-locked-documents': PayloadLockedDocument;
|
'payload-locked-documents': PayloadLockedDocument;
|
||||||
'payload-preferences': PayloadPreference;
|
'payload-preferences': PayloadPreference;
|
||||||
'payload-migrations': PayloadMigration;
|
'payload-migrations': PayloadMigration;
|
||||||
@@ -131,6 +132,7 @@ export interface Config {
|
|||||||
'custom-list-drawer': CustomListDrawerSelect<false> | CustomListDrawerSelect<true>;
|
'custom-list-drawer': CustomListDrawerSelect<false> | CustomListDrawerSelect<true>;
|
||||||
'list-view-select-api': ListViewSelectApiSelect<false> | ListViewSelectApiSelect<true>;
|
'list-view-select-api': ListViewSelectApiSelect<false> | ListViewSelectApiSelect<true>;
|
||||||
virtuals: VirtualsSelect<false> | VirtualsSelect<true>;
|
virtuals: VirtualsSelect<false> | VirtualsSelect<true>;
|
||||||
|
'no-timestamps': NoTimestampsSelect<false> | NoTimestampsSelect<true>;
|
||||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||||
@@ -612,6 +614,14 @@ export interface Virtual {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: 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
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "payload-locked-documents".
|
* via the `definition` "payload-locked-documents".
|
||||||
@@ -734,6 +744,10 @@ export interface PayloadLockedDocument {
|
|||||||
| ({
|
| ({
|
||||||
relationTo: 'virtuals';
|
relationTo: 'virtuals';
|
||||||
value: string | Virtual;
|
value: string | Virtual;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'no-timestamps';
|
||||||
|
value: string | NoTimestamp;
|
||||||
} | null);
|
} | null);
|
||||||
globalSlug?: string | null;
|
globalSlug?: string | null;
|
||||||
user: {
|
user: {
|
||||||
@@ -1175,6 +1189,13 @@ export interface VirtualsSelect<T extends boolean = true> {
|
|||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: 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
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "payload-locked-documents_select".
|
* via the `definition` "payload-locked-documents_select".
|
||||||
|
|||||||
Reference in New Issue
Block a user