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:
@@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user