feat(plugin-import-export): adds limit and page fields to export options (#13380)

### What:
This PR adds `limit` and `page` fields to the export options, allowing
users to control the number of documents exported and the page from
which to start the export. It also enforces that limit must be a
positive multiple of 100.

### Why:
This feature is needed to provide pagination support for large exports,
enabling users to export manageable chunks of data rather than the
entire dataset at once. Enforcing multiples-of-100 for `limit` ensures
consistent chunking behavior and prevents unexpected export issues.

### How:
- The `limit` field determines the maximum number of documents to export
and **must be a positive multiple of 100**.
- The `page` field defines the starting page of the export and is
displayed only when a `limit` is specified.
- If `limit` is cleared, the `page` resets to 1 to maintain consistency.
- Export logic was adjusted to respect the `limit` and `page` values
when fetching documents.

---------

Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
This commit is contained in:
Dan Ribbens
2025-08-13 17:01:45 -04:00
committed by GitHub
parent 3e65111bc1
commit c1c68fbb55
56 changed files with 415 additions and 50 deletions

View File

@@ -87,6 +87,129 @@ describe('@payloadcms/plugin-import-export', () => {
expect(data[0].updatedAt).toBeDefined()
})
it('should create a file for collection csv with all documents when limit 0', async () => {
let doc = await payload.create({
collection: 'exports',
user,
data: {
collectionSlug: 'pages',
format: 'csv',
limit: 0,
},
})
doc = await payload.findByID({
collection: 'exports',
id: doc.id,
})
expect(doc.filename).toBeDefined()
const expectedPath = path.join(dirname, './uploads', doc.filename as string)
const data = await readCSV(expectedPath)
expect(data).toHaveLength(250)
})
it('should create a file for collection csv with all documents when no limit', async () => {
let doc = await payload.create({
collection: 'exports',
user,
data: {
collectionSlug: 'pages',
format: 'csv',
},
})
doc = await payload.findByID({
collection: 'exports',
id: doc.id,
})
expect(doc.filename).toBeDefined()
const expectedPath = path.join(dirname, './uploads', doc.filename as string)
const data = await readCSV(expectedPath)
expect(data).toHaveLength(250)
})
it('should create a file for collection csv from limit and page 1', async () => {
let doc = await payload.create({
collection: 'exports',
user,
data: {
collectionSlug: 'pages',
format: 'csv',
limit: 100,
page: 1,
},
})
doc = await payload.findByID({
collection: 'exports',
id: doc.id,
})
expect(doc.filename).toBeDefined()
const expectedPath = path.join(dirname, './uploads', doc.filename as string)
const data = await readCSV(expectedPath)
expect(data[0].id).toBeDefined()
expect(data[0].title).toStrictEqual('Polymorphic 4')
})
it('should create a file for collection csv from limit and page 2', async () => {
let doc = await payload.create({
collection: 'exports',
user,
data: {
collectionSlug: 'pages',
format: 'csv',
limit: 100,
page: 2,
},
})
doc = await payload.findByID({
collection: 'exports',
id: doc.id,
})
expect(doc.filename).toBeDefined()
const expectedPath = path.join(dirname, './uploads', doc.filename as string)
const data = await readCSV(expectedPath)
expect(data[0].id).toBeDefined()
expect(data[0].title).toStrictEqual('Doc 149')
})
it('should not create a file for collection csv when limit < 0', async () => {
await expect(
payload.create({
collection: 'exports',
user,
data: {
collectionSlug: 'pages',
format: 'csv',
limit: -1,
},
}),
).rejects.toThrow(/Limit/)
})
it('should not create a file for collection csv when limit is not a multiple of 100', async () => {
await expect(
payload.create({
collection: 'exports',
user,
data: {
collectionSlug: 'pages',
format: 'csv',
limit: 99,
},
}),
).rejects.toThrow(/Limit/)
})
it('should create a file for collection csv with draft data', async () => {
const draftPage = await payload.create({
collection: 'pages',

View File

@@ -268,6 +268,7 @@ export interface Export {
name?: string | null;
format?: ('csv' | 'json') | null;
limit?: number | null;
page?: number | null;
sort?: string | null;
locale?: ('all' | 'en' | 'es' | 'de') | null;
drafts?: ('yes' | 'no') | null;
@@ -304,6 +305,7 @@ export interface ExportsTask {
name?: string | null;
format?: ('csv' | 'json') | null;
limit?: number | null;
page?: number | null;
sort?: string | null;
locale?: ('all' | 'en' | 'es' | 'de') | null;
drafts?: ('yes' | 'no') | null;
@@ -605,6 +607,7 @@ export interface ExportsSelect<T extends boolean = true> {
name?: T;
format?: T;
limit?: T;
page?: T;
sort?: T;
locale?: T;
drafts?: T;
@@ -632,6 +635,7 @@ export interface ExportsTasksSelect<T extends boolean = true> {
name?: T;
format?: T;
limit?: T;
page?: T;
sort?: T;
locale?: T;
drafts?: T;
@@ -723,6 +727,7 @@ export interface TaskCreateCollectionExport {
name?: string | null;
format?: ('csv' | 'json') | null;
limit?: number | null;
page?: number | null;
sort?: string | null;
locale?: ('all' | 'en' | 'es' | 'de') | null;
drafts?: ('yes' | 'no') | null;

View File

@@ -26,6 +26,15 @@ export const seed = async (payload: Payload): Promise<boolean> => {
posts.push(post)
}
// create pages
for (let i = 0; i < 195; i++) {
await payload.create({
collection: 'pages',
data: {
title: `Doc ${i}`,
},
})
}
for (let i = 0; i < 5; i++) {
await payload.create({
collection: 'pages',