Skip to content

Commit 0806ee1

Browse files
authored
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: [] }`).
1 parent e99c67f commit 0806ee1

File tree

6 files changed

+145
-82
lines changed

6 files changed

+145
-82
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
'use client'
2+
3+
import type { Where } from 'payload'
4+
5+
import {
6+
RadioGroupField,
7+
useDocumentInfo,
8+
useField,
9+
useListQuery,
10+
useSelection,
11+
useTranslation,
12+
} from '@payloadcms/ui'
13+
import React, { useEffect, useMemo } from 'react'
14+
15+
const isWhereEmpty = (where: Where): boolean => {
16+
if (!where || typeof where !== 'object') {
17+
return true
18+
}
19+
20+
// Flatten one level of OR/AND wrappers
21+
if (Array.isArray(where.and)) {
22+
return where.and.length === 0
23+
}
24+
if (Array.isArray(where.or)) {
25+
return where.or.length === 0
26+
}
27+
28+
return Object.keys(where).length === 0
29+
}
30+
31+
export const SelectionToUseField: React.FC = () => {
32+
const { id } = useDocumentInfo()
33+
const { query } = useListQuery()
34+
const { selectAll, selected } = useSelection()
35+
const { t } = useTranslation()
36+
37+
const { setValue: setSelectionToUseValue, value: selectionToUseValue } = useField({
38+
path: 'selectionToUse',
39+
})
40+
41+
const { setValue: setWhere } = useField({
42+
path: 'where',
43+
})
44+
45+
const hasMeaningfulFilters = query?.where && !isWhereEmpty(query.where)
46+
47+
const availableOptions = useMemo(() => {
48+
const options = [
49+
{
50+
// @ts-expect-error - this is not correctly typed in plugins right now
51+
label: t('plugin-import-export:selectionToUse-allDocuments'),
52+
value: 'all',
53+
},
54+
]
55+
56+
if (hasMeaningfulFilters) {
57+
options.unshift({
58+
// @ts-expect-error - this is not correctly typed in plugins right now
59+
label: t('plugin-import-export:selectionToUse-currentFilters'),
60+
value: 'currentFilters',
61+
})
62+
}
63+
64+
if (['allInPage', 'some'].includes(selectAll)) {
65+
options.unshift({
66+
// @ts-expect-error - this is not correctly typed in plugins right now
67+
label: t('plugin-import-export:selectionToUse-currentSelection'),
68+
value: 'currentSelection',
69+
})
70+
}
71+
72+
return options
73+
}, [hasMeaningfulFilters, selectAll, t])
74+
75+
// Auto-set default
76+
useEffect(() => {
77+
if (id) {
78+
return
79+
}
80+
81+
let defaultSelection: 'all' | 'currentFilters' | 'currentSelection' = 'all'
82+
83+
if (['allInPage', 'some'].includes(selectAll)) {
84+
defaultSelection = 'currentSelection'
85+
} else if (query?.where) {
86+
defaultSelection = 'currentFilters'
87+
}
88+
89+
setSelectionToUseValue(defaultSelection)
90+
}, [id, selectAll, query?.where, setSelectionToUseValue])
91+
92+
// Sync where clause with selected option
93+
useEffect(() => {
94+
if (id) {
95+
return
96+
}
97+
98+
if (selectionToUseValue === 'currentFilters' && query?.where) {
99+
setWhere(query.where)
100+
} else if (selectionToUseValue === 'currentSelection' && selected) {
101+
const ids = [...selected.entries()].filter(([_, isSelected]) => isSelected).map(([id]) => id)
102+
103+
setWhere({ id: { in: ids } })
104+
} else if (selectionToUseValue === 'all') {
105+
setWhere({})
106+
}
107+
}, [id, selectionToUseValue, query?.where, selected, setWhere])
108+
109+
// Hide component if no other options besides "all" are available
110+
if (availableOptions.length <= 1) {
111+
return null
112+
}
113+
114+
return (
115+
<RadioGroupField
116+
field={{
117+
name: 'selectionToUse',
118+
type: 'radio',
119+
admin: {},
120+
// @ts-expect-error - this is not correctly typed in plugins right now
121+
label: t('plugin-import-export:field-selectionToUse-label'),
122+
options: availableOptions,
123+
}}
124+
// @ts-expect-error - this is not correctly typed in plugins right now
125+
label={t('plugin-import-export:field-selectionToUse-label')}
126+
options={availableOptions}
127+
path="selectionToUse"
128+
/>
129+
)
130+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export const SortBy: SelectFieldClientComponent = (props) => {
4646
if (option && (!displayedValue || displayedValue.value !== value)) {
4747
setDisplayedValue(option)
4848
}
49-
}, [value, fieldOptions])
49+
}, [displayedValue, fieldOptions, value])
5050

5151
useEffect(() => {
5252
if (id || !query?.sort || value) {

packages/plugin-import-export/src/components/WhereField/index.scss

Whitespace-only changes.

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

Lines changed: 0 additions & 72 deletions
This file was deleted.

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

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,13 @@ export const getFields = (config: Config): Field[] => {
132132
],
133133
},
134134
{
135-
// virtual field for the UI component to modify the hidden `where` field
136135
name: 'selectionToUse',
137136
type: 'radio',
138-
defaultValue: 'all',
139-
// @ts-expect-error - this is not correctly typed in plugins right now
140-
label: ({ t }) => t('plugin-import-export:field-selectionToUse-label'),
137+
admin: {
138+
components: {
139+
Field: '@payloadcms/plugin-import-export/rsc#SelectionToUseField',
140+
},
141+
},
141142
options: [
142143
{
143144
// @ts-expect-error - this is not correctly typed in plugins right now
@@ -155,7 +156,6 @@ export const getFields = (config: Config): Field[] => {
155156
value: 'all',
156157
},
157158
],
158-
virtual: true,
159159
},
160160
{
161161
name: 'fields',
@@ -184,11 +184,16 @@ export const getFields = (config: Config): Field[] => {
184184
name: 'where',
185185
type: 'json',
186186
admin: {
187-
components: {
188-
Field: '@payloadcms/plugin-import-export/rsc#WhereField',
189-
},
187+
hidden: true,
190188
},
191189
defaultValue: {},
190+
hooks: {
191+
beforeValidate: [
192+
({ value }) => {
193+
return value ?? {}
194+
},
195+
],
196+
},
192197
},
193198
],
194199
// @ts-expect-error - this is not correctly typed in plugins right now

packages/plugin-import-export/src/exports/rsc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ export { ExportSaveButton } from '../components/ExportSaveButton/index.js'
44
export { FieldsToExport } from '../components/FieldsToExport/index.js'
55
export { ImportExportProvider } from '../components/ImportExportProvider/index.js'
66
export { Preview } from '../components/Preview/index.js'
7+
export { SelectionToUseField } from '../components/SelectionToUseField/index.js'
78
export { SortBy } from '../components/SortBy/index.js'
8-
export { WhereField } from '../components/WhereField/index.js'

0 commit comments

Comments
 (0)