Files
payload/packages/plugin-import-export/src/index.ts
Patrik 5839cb61fa feat(plugin-import-export): adds support for disabling fields (#13166)
### What?

Adds support for excluding specific fields from the import-export plugin
using a custom field config.

### Why?

Some fields should not be included in exports or previews. This feature
allows users to flag those fields directly in the field config.

### How?

- Introduced a `plugin-import-export.disabled: true` custom field
property.
- Automatically collects and stores disabled field accessors in
`collection.admin.custom['plugin-import-export'].disabledFields`.
- Excludes these fields from the export field selector, preview table,
and final export output (CSV/JSON).
2025-07-14 17:10:36 -04:00

196 lines
6.2 KiB
TypeScript

import type { Config, FlattenedField } from 'payload'
import { addDataAndFileToRequest, deepMergeSimple, flattenTopLevelFields } from 'payload'
import type { PluginDefaultTranslationsObject } from './translations/types.js'
import type { ImportExportPluginConfig, ToCSVFunction } from './types.js'
import { flattenObject } from './export/flattenObject.js'
import { getCreateCollectionExportTask } from './export/getCreateExportCollectionTask.js'
import { getCustomFieldFunctions } from './export/getCustomFieldFunctions.js'
import { getSelect } from './export/getSelect.js'
import { getExportCollection } from './getExportCollection.js'
import { translations } from './translations/index.js'
import { getFlattenedFieldKeys } from './utilities/getFlattenedFieldKeys.js'
export const importExportPlugin =
(pluginConfig: ImportExportPluginConfig) =>
(config: Config): Config => {
const exportCollection = getExportCollection({ config, pluginConfig })
if (config.collections) {
config.collections.push(exportCollection)
} else {
config.collections = [exportCollection]
}
// inject custom import export provider
config.admin = config.admin || {}
config.admin.components = config.admin.components || {}
config.admin.components.providers = config.admin.components.providers || []
config.admin.components.providers.push(
'@payloadcms/plugin-import-export/rsc#ImportExportProvider',
)
// inject the createExport job into the config
;((config.jobs ??= {}).tasks ??= []).push(getCreateCollectionExportTask(config, pluginConfig))
let collectionsToUpdate = config.collections
const usePluginCollections = pluginConfig.collections && pluginConfig.collections?.length > 0
if (usePluginCollections) {
collectionsToUpdate = config.collections?.filter((collection) => {
return pluginConfig.collections?.includes(collection.slug)
})
}
collectionsToUpdate.forEach((collection) => {
if (!collection.admin) {
collection.admin = { components: { listMenuItems: [] } }
}
const components = collection.admin.components || {}
if (!components.listMenuItems) {
components.listMenuItems = []
}
components.listMenuItems.push({
clientProps: {
exportCollectionSlug: exportCollection.slug,
},
path: '@payloadcms/plugin-import-export/rsc#ExportListMenuItem',
})
// Flatten top-level fields to expose nested fields for export config
const flattenedFields = flattenTopLevelFields(collection.fields, {
moveSubFieldsToTop: true,
})
// Find fields explicitly marked as disabled for import/export
const disabledFieldAccessors = flattenedFields
.filter((field) => field.custom?.['plugin-import-export']?.disabled)
.map((field) => field.accessor || field.name)
// Store disabled field accessors in the admin config for use in the UI
collection.admin.custom = {
...(collection.admin.custom || {}),
'plugin-import-export': {
...(collection.admin.custom?.['plugin-import-export'] || {}),
disabledFields: disabledFieldAccessors,
},
}
collection.admin.components = components
})
if (!config.i18n) {
config.i18n = {}
}
// config.i18n.translations = deepMergeSimple(translations, config.i18n?.translations ?? {})
// Inject custom REST endpoints into the config
config.endpoints = config.endpoints || []
config.endpoints.push({
handler: async (req) => {
await addDataAndFileToRequest(req)
const { collectionSlug, draft, fields, limit, locale, sort, where } = req.data as {
collectionSlug: string
draft?: 'no' | 'yes'
fields?: string[]
limit?: number
locale?: string
sort?: any
where?: any
}
const collection = req.payload.collections[collectionSlug]
if (!collection) {
return Response.json(
{ error: `Collection with slug ${collectionSlug} not found` },
{ status: 400 },
)
}
const select = Array.isArray(fields) && fields.length > 0 ? getSelect(fields) : undefined
const result = await req.payload.find({
collection: collectionSlug,
depth: 1,
draft: draft === 'yes',
limit: limit && limit > 10 ? 10 : limit,
locale,
overrideAccess: false,
req,
select,
sort,
where,
})
const docs = result.docs
const toCSVFunctions = getCustomFieldFunctions({
fields: collection.config.fields as FlattenedField[],
})
const possibleKeys = getFlattenedFieldKeys(collection.config.fields as FlattenedField[])
const transformed = docs.map((doc) => {
const row = flattenObject({
doc,
fields,
toCSVFunctions,
})
for (const key of possibleKeys) {
if (!(key in row)) {
row[key] = null
}
}
return row
})
return Response.json({
docs: transformed,
totalDocs: result.totalDocs,
})
},
method: 'post',
path: '/preview-data',
})
/**
* Merge plugin translations
*/
const simplifiedTranslations = Object.entries(translations).reduce(
(acc, [key, value]) => {
acc[key] = value.translations
return acc
},
{} as Record<string, PluginDefaultTranslationsObject>,
)
config.i18n = {
...config.i18n,
translations: deepMergeSimple(simplifiedTranslations, config.i18n?.translations ?? {}),
}
return config
}
declare module 'payload' {
export interface FieldCustom {
'plugin-import-export'?: {
/**
* When `true` the field is **completely excluded** from the import-export plugin:
* - It will not appear in the “Fields to export” selector.
* - It is hidden from the preview list when no specific fields are chosen.
* - Its data is omitted from the final CSV / JSON export.
* @default false
*/
disabled?: boolean
toCSV?: ToCSVFunction
}
}
}