feat(plugin-import-export): adds sort order control and sync with sort-by field (#13478)

### What?

This PR adds a dedicated `sortOrder` select field (Ascending /
Descending) to the import-export plugin, alongside updates to the
existing `SortBy` component. The new field and component logic keep the
sort field (`sort`) in sync with the selected sort direction.

### Why?

Previously, descending sorting did not work. While the `SortBy` field
could read the list view’s `query.sort` param, if the value contained a
leading dash (e.g. `-title`), it would not be handled correctly. Only
ascending sorts such as `sort=title` worked, and the preview table would
not reflect a descending order.

### How?

- Added a new `sortOrder` select field to the export options schema.
- Implemented a `SortOrder` custom component using ReactSelect:
- On new exports, reads `query.sort` from the list view and sets both
`sortOrder` and `sort` (combined value with or without a leading dash).
  - Handles external changes to `sort` that include a leading dash.
- Updated `SortBy`:
- No longer writes to `sort` during initial hydration—`SortOrder` owns
initial value setting.
- On user field changes, writes the combined value using the current
`sortOrder`.
This commit is contained in:
Patrik
2025-08-15 11:27:54 -04:00
committed by GitHub
parent 217606ac20
commit efdf00200a
49 changed files with 271 additions and 14 deletions

View File

@@ -227,6 +227,60 @@ describe('@payloadcms/plugin-import-export', () => {
).rejects.toThrow(/Limit/)
})
it('should export results sorted ASC by title when sort="title"', async () => {
let doc = await payload.create({
collection: 'exports',
user,
data: {
collectionSlug: 'pages',
format: 'csv',
sort: 'title',
where: {
or: [{ title: { contains: 'Title' } }, { title: { contains: 'Array' } }],
},
},
})
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('Array 0')
})
it('should export results sorted DESC by title when sort="-title"', async () => {
let doc = await payload.create({
collection: 'exports',
user,
data: {
collectionSlug: 'pages',
format: 'csv',
sort: '-title',
where: {
or: [{ title: { contains: 'Title' } }, { title: { contains: 'Array' } }],
},
},
})
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('Title 4')
})
it('should create a file for collection csv with draft data', async () => {
const draftPage = await payload.create({
collection: 'pages',

View File

@@ -270,6 +270,7 @@ export interface Export {
limit?: number | null;
page?: number | null;
sort?: string | null;
sortOrder?: ('asc' | 'desc') | null;
locale?: ('all' | 'en' | 'es' | 'de') | null;
drafts?: ('yes' | 'no') | null;
selectionToUse?: ('currentSelection' | 'currentFilters' | 'all') | null;
@@ -307,6 +308,7 @@ export interface ExportsTask {
limit?: number | null;
page?: number | null;
sort?: string | null;
sortOrder?: ('asc' | 'desc') | null;
locale?: ('all' | 'en' | 'es' | 'de') | null;
drafts?: ('yes' | 'no') | null;
selectionToUse?: ('currentSelection' | 'currentFilters' | 'all') | null;
@@ -609,6 +611,7 @@ export interface ExportsSelect<T extends boolean = true> {
limit?: T;
page?: T;
sort?: T;
sortOrder?: T;
locale?: T;
drafts?: T;
selectionToUse?: T;
@@ -637,6 +640,7 @@ export interface ExportsTasksSelect<T extends boolean = true> {
limit?: T;
page?: T;
sort?: T;
sortOrder?: T;
locale?: T;
drafts?: T;
selectionToUse?: T;
@@ -729,6 +733,7 @@ export interface TaskCreateCollectionExport {
limit?: number | null;
page?: number | null;
sort?: string | null;
sortOrder?: ('asc' | 'desc') | null;
locale?: ('all' | 'en' | 'es' | 'de') | null;
drafts?: ('yes' | 'no') | null;
selectionToUse?: ('currentSelection' | 'currentFilters' | 'all') | null;