Skip to content

Commit a7ed88b

Browse files
authored
feat(plugin-import-export): use groupBy as SortBy when present and sort is unset (#13491)
### What? When exporting, if no `sort` parameter is set but a `groupBy` parameter is present in the list-view query, the export will treat `groupBy` as the SortBy field and default to ascending order. Additionally, the SortOrder field in the export UI is now hidden when no sort is present, reducing visual noise and preventing irrelevant order selection. ### Why? Previously, exports ignored `groupBy` entirely when no sort was set, leading to unsorted output even if the list view was grouped. Also, SortOrder was always shown, even when no sort field was selected, which could be confusing. These changes ensure exports reflect the list view’s grouping and keep the UI focused. ### How? - Check for `groupBy` in the query only when `sort` is unset. - If found, set SortBy to `groupBy` and SortOrder to ascending. - Hide the SortOrder field when `sort` is not set. - Leave sorting unset if neither `sort` nor `groupBy` are present.
1 parent ec5b673 commit a7ed88b

File tree

5 files changed

+90
-31
lines changed

5 files changed

+90
-31
lines changed

packages/plugin-import-export/src/components/SortBy/index.tsx

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import {
1111
useField,
1212
useListQuery,
1313
} from '@payloadcms/ui'
14-
import React, { useEffect, useMemo, useState } from 'react'
14+
import React, { useEffect, useMemo, useRef, useState } from 'react'
1515

16-
import { applySortOrder, stripSortDash } from '../../utilities/sortHelpers.js'
16+
import { applySortOrder, normalizeQueryParam, stripSortDash } from '../../utilities/sortHelpers.js'
1717
import { reduceFields } from '../FieldsToExport/reduceFields.js'
1818
import { useImportExport } from '../ImportExportProvider/index.js'
1919
import './index.scss'
@@ -28,6 +28,8 @@ export const SortBy: SelectFieldClientComponent = (props) => {
2828

2929
// Sibling order field ('asc' | 'desc') used when writing sort on change
3030
const { value: sortOrder = 'asc' } = useField<string>({ path: 'sortOrder' })
31+
// Needed so we can initialize sortOrder when SortOrder component is hidden
32+
const { setValue: setSortOrder } = useField<'asc' | 'desc'>({ path: 'sortOrder' })
3133

3234
const { value: collectionSlug } = useField<string>({ path: 'collectionSlug' })
3335
const { query } = useListQuery()
@@ -61,23 +63,49 @@ export const SortBy: SelectFieldClientComponent = (props) => {
6163
}
6264
}, [sortRaw, fieldOptions, displayedValue])
6365

64-
// Sync the visible select from list-view query sort,
65-
// but no need to write to the "sort" field here — SortOrder owns initial combined value.
66+
// One-time init guard so clearing `sort` doesn't rehydrate from query again
67+
const didInitRef = useRef(false)
68+
69+
// Sync the visible select from list-view query sort (preferred) or groupBy (fallback)
70+
// and initialize both `sort` and `sortOrder` here as SortOrder may be hidden by admin.condition.
6671
useEffect(() => {
67-
if (id || !query?.sort || sortRaw) {
72+
if (didInitRef.current) {
73+
return
74+
}
75+
if (id) {
76+
didInitRef.current = true
77+
return
78+
}
79+
if (typeof sortRaw === 'string' && sortRaw.length > 0) {
80+
// Already initialized elsewhere
81+
didInitRef.current = true
6882
return
6983
}
7084

71-
if (!query.sort) {
85+
const qsSort = normalizeQueryParam(query?.sort)
86+
const qsGroupBy = normalizeQueryParam(query?.groupBy)
87+
88+
const source = qsSort ?? qsGroupBy
89+
if (!source) {
90+
didInitRef.current = true
7291
return
7392
}
7493

75-
const clean = stripSortDash(query.sort as string)
76-
const option = fieldOptions.find((f) => f.value === clean)
94+
const isDesc = !!qsSort && qsSort.startsWith('-')
95+
const base = stripSortDash(source)
96+
const order: 'asc' | 'desc' = isDesc ? 'desc' : 'asc'
97+
98+
// Write BOTH fields so preview/export have the right values even if SortOrder is hidden
99+
setSort(applySortOrder(base, order))
100+
setSortOrder(order)
101+
102+
const option = fieldOptions.find((f) => f.value === base)
77103
if (option) {
78104
setDisplayedValue(option)
79105
}
80-
}, [id, query?.sort, sortRaw, fieldOptions])
106+
107+
didInitRef.current = true
108+
}, [id, query?.groupBy, query?.sort, sortRaw, fieldOptions, setSort, setSortOrder])
81109

82110
// When user selects a different field, store it with the current order applied
83111
const onChange = (option: { id: string; label: ReactNode; value: string } | null) => {

packages/plugin-import-export/src/components/SortOrder/index.tsx

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import type { SelectFieldClientComponent } from 'payload'
44

55
import { FieldLabel, ReactSelect, useDocumentInfo, useField, useListQuery } from '@payloadcms/ui'
6-
import React, { useEffect, useMemo, useState } from 'react'
6+
import React, { useEffect, useMemo, useRef, useState } from 'react'
77

8-
import { applySortOrder, stripSortDash } from '../../utilities/sortHelpers.js'
8+
import { applySortOrder, normalizeQueryParam, stripSortDash } from '../../utilities/sortHelpers.js'
99
import './index.scss'
1010

1111
const baseClass = 'sort-order-field'
@@ -20,17 +20,6 @@ const options = [
2020

2121
const defaultOption: OrderOption = options[0]
2222

23-
// Safely coerce query.sort to a string (ignore arrays)
24-
const normalizeSortParam = (v: unknown): string | undefined => {
25-
if (typeof v === 'string') {
26-
return v
27-
}
28-
if (Array.isArray(v) && typeof v[0] === 'string') {
29-
return v[0]
30-
}
31-
return undefined
32-
}
33-
3423
export const SortOrder: SelectFieldClientComponent = (props) => {
3524
const { id } = useDocumentInfo()
3625
const { query } = useListQuery()
@@ -51,23 +40,51 @@ export const SortOrder: SelectFieldClientComponent = (props) => {
5140
)
5241
const [displayed, setDisplayed] = useState<null | OrderOption>(currentOption)
5342

54-
// Derive from list-view query.sort if present
43+
// One-time init guard so clearing `sort` doesn't rehydrate from query again
44+
const didInitRef = useRef(false)
45+
46+
// Derive from list-view query.sort if present; otherwise fall back to groupBy
5547
useEffect(() => {
48+
if (didInitRef.current) {
49+
return
50+
}
51+
52+
// Existing export -> don't initialize here
5653
if (id) {
54+
didInitRef.current = true
5755
return
5856
}
59-
const qs = normalizeSortParam(query?.sort)
60-
if (!qs) {
57+
58+
// If sort already has a value, treat as initialized
59+
if (typeof sortRaw === 'string' && sortRaw.length > 0) {
60+
didInitRef.current = true
6161
return
6262
}
6363

64-
const isDesc = qs.startsWith('-')
65-
const base = stripSortDash(qs)
66-
const order: Order = isDesc ? 'desc' : 'asc'
64+
const qsSort = normalizeQueryParam(query?.sort)
65+
const qsGroupBy = normalizeQueryParam(query?.groupBy)
66+
67+
if (qsSort) {
68+
const isDesc = qsSort.startsWith('-')
69+
const base = stripSortDash(qsSort)
70+
const order: Order = isDesc ? 'desc' : 'asc'
71+
setOrder(order)
72+
setSort(applySortOrder(base, order)) // combined: 'title' or '-title'
73+
didInitRef.current = true
74+
return
75+
}
76+
77+
// Fallback: groupBy (always ascending)
78+
if (qsGroupBy) {
79+
setOrder('asc')
80+
setSort(applySortOrder(qsGroupBy, 'asc')) // write 'groupByField' (no dash)
81+
didInitRef.current = true
82+
return
83+
}
6784

68-
setOrder(order)
69-
setSort(applySortOrder(base, order))
70-
}, [id, query?.sort, setOrder, setSort])
85+
// Nothing to initialize
86+
didInitRef.current = true
87+
}, [id, query?.sort, query?.groupBy, sortRaw, setOrder, setSort])
7188

7289
// Keep the select's displayed option in sync with the stored order
7390
useEffect(() => {

packages/plugin-import-export/src/export/getFields.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ export const getFields = (config: Config, pluginConfig?: ImportExportPluginConfi
125125
components: {
126126
Field: '@payloadcms/plugin-import-export/rsc#SortOrder',
127127
},
128+
// Only show when `sort` has a value
129+
condition: ({ sort }) => typeof sort === 'string' && sort.trim().length > 0,
128130
},
129131
// @ts-expect-error - this is not correctly typed in plugins right now
130132
label: ({ t }) => t('plugin-import-export:field-sort-order-label'),

packages/plugin-import-export/src/utilities/sortHelpers.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,14 @@ export const stripSortDash = (v?: null | string): string => (v ? v.replace(/^-/,
44
/** Apply order to a base field (("title","desc") -> "-title") */
55
export const applySortOrder = (field: string, order: 'asc' | 'desc'): string =>
66
order === 'desc' ? `-${field}` : field
7+
8+
// Safely coerce query.sort / query.groupBy to a string (ignore arrays)
9+
export const normalizeQueryParam = (v: unknown): string | undefined => {
10+
if (typeof v === 'string') {
11+
return v
12+
}
13+
if (Array.isArray(v) && typeof v[0] === 'string') {
14+
return v[0]
15+
}
16+
return undefined
17+
}

test/plugin-import-export/collections/Pages.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const Pages: CollectionConfig = {
1010
},
1111
admin: {
1212
useAsTitle: 'title',
13+
groupBy: true,
1314
},
1415
versions: {
1516
drafts: true,

0 commit comments

Comments
 (0)