fix(plugin-import-export): selectionToUse field to dynamically show valid export options (#13092)

### What?

Updated the `selectionToUse` export field to properly render a radio
group with dynamic options based on current selection state and applied
filters.

- Fixed an edge case where `currentFilters` would appear as an option
even when the `where` clause was empty (e.g. `{ or: [] }`).

### Why?

Previously, the `selectionToUse` field displayed all options (current
selection, current filters, all documents) regardless of context. This
caused confusion when only one of them was applicable.

### How?

- Added a custom field component that dynamically computes available
options based on:
  - Current filters from `useListQuery`
  - Selection state from `useSelection`
- Injected the dynamic `field` prop into `RadioGroupField` to enable
rendering.
- Ensured the `where` field updates automatically in sync with the
selected radio.
- Added `isWhereEmpty` utility to avoid showing `currentFilters` when
`query.where` contains no meaningful conditions (e.g. `{ or: [] }`).
This commit is contained in:
Patrik
2025-07-09 15:44:22 -04:00
committed by GitHub
parent e99c67f5f9
commit 0806ee1762
6 changed files with 145 additions and 82 deletions

View File

@@ -0,0 +1,130 @@
'use client'
import type { Where } from 'payload'
import {
RadioGroupField,
useDocumentInfo,
useField,
useListQuery,
useSelection,
useTranslation,
} from '@payloadcms/ui'
import React, { useEffect, useMemo } from 'react'
const isWhereEmpty = (where: Where): boolean => {
if (!where || typeof where !== 'object') {
return true
}
// Flatten one level of OR/AND wrappers
if (Array.isArray(where.and)) {
return where.and.length === 0
}
if (Array.isArray(where.or)) {
return where.or.length === 0
}
return Object.keys(where).length === 0
}
export const SelectionToUseField: React.FC = () => {
const { id } = useDocumentInfo()
const { query } = useListQuery()
const { selectAll, selected } = useSelection()
const { t } = useTranslation()
const { setValue: setSelectionToUseValue, value: selectionToUseValue } = useField({
path: 'selectionToUse',
})
const { setValue: setWhere } = useField({
path: 'where',
})
const hasMeaningfulFilters = query?.where && !isWhereEmpty(query.where)
const availableOptions = useMemo(() => {
const options = [
{
// @ts-expect-error - this is not correctly typed in plugins right now
label: t('plugin-import-export:selectionToUse-allDocuments'),
value: 'all',
},
]
if (hasMeaningfulFilters) {
options.unshift({
// @ts-expect-error - this is not correctly typed in plugins right now
label: t('plugin-import-export:selectionToUse-currentFilters'),
value: 'currentFilters',
})
}
if (['allInPage', 'some'].includes(selectAll)) {
options.unshift({
// @ts-expect-error - this is not correctly typed in plugins right now
label: t('plugin-import-export:selectionToUse-currentSelection'),
value: 'currentSelection',
})
}
return options
}, [hasMeaningfulFilters, selectAll, t])
// Auto-set default
useEffect(() => {
if (id) {
return
}
let defaultSelection: 'all' | 'currentFilters' | 'currentSelection' = 'all'
if (['allInPage', 'some'].includes(selectAll)) {
defaultSelection = 'currentSelection'
} else if (query?.where) {
defaultSelection = 'currentFilters'
}
setSelectionToUseValue(defaultSelection)
}, [id, selectAll, query?.where, setSelectionToUseValue])
// Sync where clause with selected option
useEffect(() => {
if (id) {
return
}
if (selectionToUseValue === 'currentFilters' && query?.where) {
setWhere(query.where)
} else if (selectionToUseValue === 'currentSelection' && selected) {
const ids = [...selected.entries()].filter(([_, isSelected]) => isSelected).map(([id]) => id)
setWhere({ id: { in: ids } })
} else if (selectionToUseValue === 'all') {
setWhere({})
}
}, [id, selectionToUseValue, query?.where, selected, setWhere])
// Hide component if no other options besides "all" are available
if (availableOptions.length <= 1) {
return null
}
return (
<RadioGroupField
field={{
name: 'selectionToUse',
type: 'radio',
admin: {},
// @ts-expect-error - this is not correctly typed in plugins right now
label: t('plugin-import-export:field-selectionToUse-label'),
options: availableOptions,
}}
// @ts-expect-error - this is not correctly typed in plugins right now
label={t('plugin-import-export:field-selectionToUse-label')}
options={availableOptions}
path="selectionToUse"
/>
)
}

View File

@@ -46,7 +46,7 @@ export const SortBy: SelectFieldClientComponent = (props) => {
if (option && (!displayedValue || displayedValue.value !== value)) {
setDisplayedValue(option)
}
}, [value, fieldOptions])
}, [displayedValue, fieldOptions, value])
useEffect(() => {
if (id || !query?.sort || value) {

View File

@@ -1,72 +0,0 @@
'use client'
import type React from 'react'
import { useDocumentInfo, useField, useListQuery, useSelection } from '@payloadcms/ui'
import { useEffect } from 'react'
import './index.scss'
export const WhereField: React.FC = () => {
const { setValue: setSelectionToUseValue, value: selectionToUseValue } = useField({
path: 'selectionToUse',
})
const { setValue } = useField({ path: 'where' })
const { selectAll, selected } = useSelection()
const { query } = useListQuery()
const { id } = useDocumentInfo()
// setValue based on selectionToUseValue
useEffect(() => {
if (id) {
return
}
if (selectionToUseValue === 'currentFilters' && query && query?.where) {
setValue(query.where)
}
if (selectionToUseValue === 'currentSelection' && selected) {
const ids = []
for (const [key, value] of selected) {
if (value) {
ids.push(key)
}
}
setValue({
id: {
in: ids,
},
})
}
if (selectionToUseValue === 'all' && selected) {
setValue({})
}
// Selected set a where query with IDs
}, [id, selectionToUseValue, query, selected, setValue])
// handles default value of selectionToUse
useEffect(() => {
if (id) {
return
}
let defaultSelection: 'all' | 'currentFilters' | 'currentSelection' = 'all'
if (['allInPage', 'some'].includes(selectAll)) {
defaultSelection = 'currentSelection'
}
if (defaultSelection === 'all' && query?.where) {
defaultSelection = 'currentFilters'
}
setSelectionToUseValue(defaultSelection)
}, [id, query, selectAll, setSelectionToUseValue])
return null
}

View File

@@ -132,12 +132,13 @@ export const getFields = (config: Config): Field[] => {
],
},
{
// virtual field for the UI component to modify the hidden `where` field
name: 'selectionToUse',
type: 'radio',
defaultValue: 'all',
// @ts-expect-error - this is not correctly typed in plugins right now
label: ({ t }) => t('plugin-import-export:field-selectionToUse-label'),
admin: {
components: {
Field: '@payloadcms/plugin-import-export/rsc#SelectionToUseField',
},
},
options: [
{
// @ts-expect-error - this is not correctly typed in plugins right now
@@ -155,7 +156,6 @@ export const getFields = (config: Config): Field[] => {
value: 'all',
},
],
virtual: true,
},
{
name: 'fields',
@@ -184,11 +184,16 @@ export const getFields = (config: Config): Field[] => {
name: 'where',
type: 'json',
admin: {
components: {
Field: '@payloadcms/plugin-import-export/rsc#WhereField',
},
hidden: true,
},
defaultValue: {},
hooks: {
beforeValidate: [
({ value }) => {
return value ?? {}
},
],
},
},
],
// @ts-expect-error - this is not correctly typed in plugins right now

View File

@@ -4,5 +4,5 @@ export { ExportSaveButton } from '../components/ExportSaveButton/index.js'
export { FieldsToExport } from '../components/FieldsToExport/index.js'
export { ImportExportProvider } from '../components/ImportExportProvider/index.js'
export { Preview } from '../components/Preview/index.js'
export { SelectionToUseField } from '../components/SelectionToUseField/index.js'
export { SortBy } from '../components/SortBy/index.js'
export { WhereField } from '../components/WhereField/index.js'