From 0806ee17620caa9f21ba652ada3cdec0c0c54ef6 Mon Sep 17 00:00:00 2001 From: Patrik <35232443+PatrikKozak@users.noreply.github.com> Date: Wed, 9 Jul 2025 15:44:22 -0400 Subject: [PATCH] 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: [] }`). --- .../components/SelectionToUseField/index.tsx | 130 ++++++++++++++++++ .../src/components/SortBy/index.tsx | 2 +- .../src/components/WhereField/index.scss | 0 .../src/components/WhereField/index.tsx | 72 ---------- .../src/export/getFields.ts | 21 +-- .../plugin-import-export/src/exports/rsc.ts | 2 +- 6 files changed, 145 insertions(+), 82 deletions(-) create mode 100644 packages/plugin-import-export/src/components/SelectionToUseField/index.tsx delete mode 100644 packages/plugin-import-export/src/components/WhereField/index.scss delete mode 100644 packages/plugin-import-export/src/components/WhereField/index.tsx diff --git a/packages/plugin-import-export/src/components/SelectionToUseField/index.tsx b/packages/plugin-import-export/src/components/SelectionToUseField/index.tsx new file mode 100644 index 0000000000..9a6d359178 --- /dev/null +++ b/packages/plugin-import-export/src/components/SelectionToUseField/index.tsx @@ -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 ( + + ) +} diff --git a/packages/plugin-import-export/src/components/SortBy/index.tsx b/packages/plugin-import-export/src/components/SortBy/index.tsx index 7680ed82c1..8952ba4d95 100644 --- a/packages/plugin-import-export/src/components/SortBy/index.tsx +++ b/packages/plugin-import-export/src/components/SortBy/index.tsx @@ -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) { diff --git a/packages/plugin-import-export/src/components/WhereField/index.scss b/packages/plugin-import-export/src/components/WhereField/index.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/plugin-import-export/src/components/WhereField/index.tsx b/packages/plugin-import-export/src/components/WhereField/index.tsx deleted file mode 100644 index 68f4daf653..0000000000 --- a/packages/plugin-import-export/src/components/WhereField/index.tsx +++ /dev/null @@ -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 -} diff --git a/packages/plugin-import-export/src/export/getFields.ts b/packages/plugin-import-export/src/export/getFields.ts index c57d84b878..c76e7de9cb 100644 --- a/packages/plugin-import-export/src/export/getFields.ts +++ b/packages/plugin-import-export/src/export/getFields.ts @@ -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 diff --git a/packages/plugin-import-export/src/exports/rsc.ts b/packages/plugin-import-export/src/exports/rsc.ts index 5072288925..9f2339b7bf 100644 --- a/packages/plugin-import-export/src/exports/rsc.ts +++ b/packages/plugin-import-export/src/exports/rsc.ts @@ -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'