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.
This commit is contained in:
@@ -11,9 +11,9 @@ import {
|
||||
useField,
|
||||
useListQuery,
|
||||
} from '@payloadcms/ui'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { applySortOrder, stripSortDash } from '../../utilities/sortHelpers.js'
|
||||
import { applySortOrder, normalizeQueryParam, stripSortDash } from '../../utilities/sortHelpers.js'
|
||||
import { reduceFields } from '../FieldsToExport/reduceFields.js'
|
||||
import { useImportExport } from '../ImportExportProvider/index.js'
|
||||
import './index.scss'
|
||||
@@ -28,6 +28,8 @@ export const SortBy: SelectFieldClientComponent = (props) => {
|
||||
|
||||
// Sibling order field ('asc' | 'desc') used when writing sort on change
|
||||
const { value: sortOrder = 'asc' } = useField<string>({ path: 'sortOrder' })
|
||||
// Needed so we can initialize sortOrder when SortOrder component is hidden
|
||||
const { setValue: setSortOrder } = useField<'asc' | 'desc'>({ path: 'sortOrder' })
|
||||
|
||||
const { value: collectionSlug } = useField<string>({ path: 'collectionSlug' })
|
||||
const { query } = useListQuery()
|
||||
@@ -61,23 +63,49 @@ export const SortBy: SelectFieldClientComponent = (props) => {
|
||||
}
|
||||
}, [sortRaw, fieldOptions, displayedValue])
|
||||
|
||||
// Sync the visible select from list-view query sort,
|
||||
// but no need to write to the "sort" field here — SortOrder owns initial combined value.
|
||||
// One-time init guard so clearing `sort` doesn't rehydrate from query again
|
||||
const didInitRef = useRef(false)
|
||||
|
||||
// Sync the visible select from list-view query sort (preferred) or groupBy (fallback)
|
||||
// and initialize both `sort` and `sortOrder` here as SortOrder may be hidden by admin.condition.
|
||||
useEffect(() => {
|
||||
if (id || !query?.sort || sortRaw) {
|
||||
if (didInitRef.current) {
|
||||
return
|
||||
}
|
||||
if (id) {
|
||||
didInitRef.current = true
|
||||
return
|
||||
}
|
||||
if (typeof sortRaw === 'string' && sortRaw.length > 0) {
|
||||
// Already initialized elsewhere
|
||||
didInitRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
if (!query.sort) {
|
||||
const qsSort = normalizeQueryParam(query?.sort)
|
||||
const qsGroupBy = normalizeQueryParam(query?.groupBy)
|
||||
|
||||
const source = qsSort ?? qsGroupBy
|
||||
if (!source) {
|
||||
didInitRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
const clean = stripSortDash(query.sort as string)
|
||||
const option = fieldOptions.find((f) => f.value === clean)
|
||||
const isDesc = !!qsSort && qsSort.startsWith('-')
|
||||
const base = stripSortDash(source)
|
||||
const order: 'asc' | 'desc' = isDesc ? 'desc' : 'asc'
|
||||
|
||||
// Write BOTH fields so preview/export have the right values even if SortOrder is hidden
|
||||
setSort(applySortOrder(base, order))
|
||||
setSortOrder(order)
|
||||
|
||||
const option = fieldOptions.find((f) => f.value === base)
|
||||
if (option) {
|
||||
setDisplayedValue(option)
|
||||
}
|
||||
}, [id, query?.sort, sortRaw, fieldOptions])
|
||||
|
||||
didInitRef.current = true
|
||||
}, [id, query?.groupBy, query?.sort, sortRaw, fieldOptions, setSort, setSortOrder])
|
||||
|
||||
// When user selects a different field, store it with the current order applied
|
||||
const onChange = (option: { id: string; label: ReactNode; value: string } | null) => {
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import type { SelectFieldClientComponent } from 'payload'
|
||||
|
||||
import { FieldLabel, ReactSelect, useDocumentInfo, useField, useListQuery } from '@payloadcms/ui'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { applySortOrder, stripSortDash } from '../../utilities/sortHelpers.js'
|
||||
import { applySortOrder, normalizeQueryParam, stripSortDash } from '../../utilities/sortHelpers.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'sort-order-field'
|
||||
@@ -20,17 +20,6 @@ const options = [
|
||||
|
||||
const defaultOption: OrderOption = options[0]
|
||||
|
||||
// Safely coerce query.sort to a string (ignore arrays)
|
||||
const normalizeSortParam = (v: unknown): string | undefined => {
|
||||
if (typeof v === 'string') {
|
||||
return v
|
||||
}
|
||||
if (Array.isArray(v) && typeof v[0] === 'string') {
|
||||
return v[0]
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const SortOrder: SelectFieldClientComponent = (props) => {
|
||||
const { id } = useDocumentInfo()
|
||||
const { query } = useListQuery()
|
||||
@@ -51,23 +40,51 @@ export const SortOrder: SelectFieldClientComponent = (props) => {
|
||||
)
|
||||
const [displayed, setDisplayed] = useState<null | OrderOption>(currentOption)
|
||||
|
||||
// Derive from list-view query.sort if present
|
||||
// One-time init guard so clearing `sort` doesn't rehydrate from query again
|
||||
const didInitRef = useRef(false)
|
||||
|
||||
// Derive from list-view query.sort if present; otherwise fall back to groupBy
|
||||
useEffect(() => {
|
||||
if (didInitRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
// Existing export -> don't initialize here
|
||||
if (id) {
|
||||
return
|
||||
}
|
||||
const qs = normalizeSortParam(query?.sort)
|
||||
if (!qs) {
|
||||
didInitRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
const isDesc = qs.startsWith('-')
|
||||
const base = stripSortDash(qs)
|
||||
const order: Order = isDesc ? 'desc' : 'asc'
|
||||
// If sort already has a value, treat as initialized
|
||||
if (typeof sortRaw === 'string' && sortRaw.length > 0) {
|
||||
didInitRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
setOrder(order)
|
||||
setSort(applySortOrder(base, order))
|
||||
}, [id, query?.sort, setOrder, setSort])
|
||||
const qsSort = normalizeQueryParam(query?.sort)
|
||||
const qsGroupBy = normalizeQueryParam(query?.groupBy)
|
||||
|
||||
if (qsSort) {
|
||||
const isDesc = qsSort.startsWith('-')
|
||||
const base = stripSortDash(qsSort)
|
||||
const order: Order = isDesc ? 'desc' : 'asc'
|
||||
setOrder(order)
|
||||
setSort(applySortOrder(base, order)) // combined: 'title' or '-title'
|
||||
didInitRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback: groupBy (always ascending)
|
||||
if (qsGroupBy) {
|
||||
setOrder('asc')
|
||||
setSort(applySortOrder(qsGroupBy, 'asc')) // write 'groupByField' (no dash)
|
||||
didInitRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
// Nothing to initialize
|
||||
didInitRef.current = true
|
||||
}, [id, query?.sort, query?.groupBy, sortRaw, setOrder, setSort])
|
||||
|
||||
// Keep the select's displayed option in sync with the stored order
|
||||
useEffect(() => {
|
||||
|
||||
@@ -125,6 +125,8 @@ export const getFields = (config: Config, pluginConfig?: ImportExportPluginConfi
|
||||
components: {
|
||||
Field: '@payloadcms/plugin-import-export/rsc#SortOrder',
|
||||
},
|
||||
// Only show when `sort` has a value
|
||||
condition: ({ sort }) => typeof sort === 'string' && sort.trim().length > 0,
|
||||
},
|
||||
// @ts-expect-error - this is not correctly typed in plugins right now
|
||||
label: ({ t }) => t('plugin-import-export:field-sort-order-label'),
|
||||
|
||||
@@ -4,3 +4,14 @@ export const stripSortDash = (v?: null | string): string => (v ? v.replace(/^-/,
|
||||
/** Apply order to a base field (("title","desc") -> "-title") */
|
||||
export const applySortOrder = (field: string, order: 'asc' | 'desc'): string =>
|
||||
order === 'desc' ? `-${field}` : field
|
||||
|
||||
// Safely coerce query.sort / query.groupBy to a string (ignore arrays)
|
||||
export const normalizeQueryParam = (v: unknown): string | undefined => {
|
||||
if (typeof v === 'string') {
|
||||
return v
|
||||
}
|
||||
if (Array.isArray(v) && typeof v[0] === 'string') {
|
||||
return v[0]
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export const Pages: CollectionConfig = {
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
groupBy: true,
|
||||
},
|
||||
versions: {
|
||||
drafts: true,
|
||||
|
||||
Reference in New Issue
Block a user