Skip to content

Commit 3830d71

Browse files
feat(plugin-import-export): preview displays CSV and JSON data accurately (#12948)
### What This PR updates the import-export plugin's `<Preview />` component to render table columns and rows using the same logic as the CSV export. Key changes: - Adds a new `/api/preview-data` custom REST endpoint that: - Accepts filters (`fields`, `where`, `sort`, `draft`, `limit`) - Uses `getCustomFieldFunctions` and `flattenObject` to transform documents - Returns deeply flattened rows identical to the CSV export - Refactors the <Preview /> component to: - POST preview config to the new endpoint instead of querying the collection directly - Match column ordering and flattening logic with the `createExport` function - Ensures consistency across CSV downloads and in-admin previews -Adds JSON preview This ensures preview results now exactly match exported CSV content, including support for custom field transformers and polymorphic fields. --------- Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
1 parent 2da6d92 commit 3830d71

2 files changed

Lines changed: 161 additions & 57 deletions

File tree

Lines changed: 93 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
'use client'
22
importtype{Column}from'@payloadcms/ui'
3-
importtype{ClientField,FieldAffectingDataClient}from'payload'
3+
importtype{ClientField}from'payload'
44

55
import{getTranslation}from'@payloadcms/translations'
6-
import{Table,Translation,useConfig,useField,useTranslation}from'@payloadcms/ui'
7-
import{fieldAffectsData}from'payload/shared'
8-
import*asqsfrom'qs-esm'
6+
import{
7+
CodeEditorLazy,
8+
Table,
9+
Translation,
10+
useConfig,
11+
useField,
12+
useTranslation,
13+
}from'@payloadcms/ui'
914
importReactfrom'react'
1015

1116
importtype{
1217
PluginImportExportTranslationKeys,
1318
PluginImportExportTranslations,
1419
}from'../../translations/index.js'
1520

16-
import'./index.scss'
1721
import{useImportExport}from'../ImportExportProvider/index.js'
22+
import'./index.scss'
1823

1924
constbaseClass='preview'
2025

@@ -25,7 +30,9 @@ export const Preview = () => {
2530
const{value: limit}=useField<number>({path: 'limit'})
2631
const{value: fields}=useField<string[]>({path: 'fields'})
2732
const{value: sort}=useField({path: 'sort'})
28-
const{value: draft}=useField({path: 'draft'})
33+
const{value: draft}=useField({path: 'drafts'})
34+
const{value: locale}=useField({path: 'locale'})
35+
const{value: format}=useField({path: 'format'})
2936
const[dataToRender,setDataToRender]=React.useState<any[]>([])
3037
const[resultCount,setResultCount]=React.useState<any>('')
3138
const[columns,setColumns]=React.useState<Column[]>([])
@@ -39,73 +46,99 @@ export const Preview = () => {
3946
(collection)=>collection.slug===collectionSlug,
4047
)
4148

49+
constisCSV=format==='csv'
50+
4251
React.useEffect(()=>{
4352
constfetchData=async()=>{
44-
if(!collectionSlug){
53+
if(!collectionSlug||!collectionConfig){
4554
return
4655
}
4756

4857
try{
49-
constwhereQuery=qs.stringify(
50-
{
51-
depth: 0,
58+
constres=awaitfetch('/api/preview-data',{
59+
body: JSON.stringify({
60+
collectionSlug,
5261
draft,
53-
limit: limit>10 ? 10 : limit,
62+
fields,
63+
limit,
64+
locale,
5465
sort,
5566
where,
56-
},
57-
{
58-
addQueryPrefix: true,
59-
},
60-
)
61-
constresponse=awaitfetch(`/api/${collectionSlug}${whereQuery}`,{
62-
headers: {
63-
'Content-Type': 'application/json',
64-
},
65-
method: 'GET',
67+
}),
68+
credentials: 'include',
69+
headers: {'Content-Type': 'application/json'},
70+
method: 'POST',
71+
})
72+
73+
if(!res.ok){
74+
return
75+
}
76+
77+
const{ docs, totalDocs }=awaitres.json()
78+
79+
setResultCount(limit&&limit<totalDocs ? limit : totalDocs)
80+
81+
constallKeys=Object.keys(docs[0]||{})
82+
constdefaultMetaFields=['createdAt','updatedAt','_status','id']
83+
84+
// Match CSV column ordering by building keys based on fields and regex
85+
constfieldToRegex=(field: string): RegExp=>{
86+
constparts=field.split('.').map((part)=>`${part}(?:_\\d+)?`)
87+
returnnewRegExp(`^${parts.join('_')}`)
88+
}
89+
90+
// Construct final list of field keys to match field order + meta order
91+
constselectedKeys=
92+
Array.isArray(fields)&&fields.length>0
93+
? fields.flatMap((field)=>{
94+
constregex=fieldToRegex(field)
95+
returnallKeys.filter((key)=>regex.test(key))
96+
})
97+
: allKeys.filter((key)=>!defaultMetaFields.includes(key))
98+
99+
constincludedMeta=newSet(selectedKeys)
100+
constmissingMetaFields=defaultMetaFields.flatMap((field)=>{
101+
constregex=fieldToRegex(field)
102+
returnallKeys.filter((key)=>regex.test(key)&&!includedMeta.has(key))
66103
})
67104

68-
if(response.ok){
69-
constdata=awaitresponse.json()
70-
setResultCount(limit&&limit<data.totalDocs ? limit : data.totalDocs)
71-
// TODO: check if this data is in the correct format for the table
105+
constfieldKeys=[...selectedKeys, ...missingMetaFields]
106+
107+
// Build columns based on flattened keys
108+
constnewColumns: Column[]=fieldKeys.map((key)=>({
109+
accessor: key,
110+
active: true,
111+
field: {name: key}asClientField,
112+
Heading: getTranslation(key,i18n),
113+
renderedCells: docs.map((doc: Record<string,unknown>)=>{
114+
constval=doc[key]
115+
116+
if(val===undefined||val===null){
117+
returnnull
118+
}
72119

73-
constfilteredFields=(collectionConfig?.fields?.filter((field)=>{
74-
if(!fieldAffectsData(field)){
75-
returnfalse
120+
// Avoid ESLint warning by type-checking before calling String()
121+
if(typeofval==='string'||typeofval==='number'||typeofval==='boolean'){
122+
returnString(val)
76123
}
77-
if(fields?.length>0){
78-
returnfields.includes(field.name)
124+
125+
if(Array.isArray(val)){
126+
returnval.map(String).join(', ')
79127
}
80-
returntrue
81-
})??[])asFieldAffectingDataClient[]
82-
83-
setColumns(
84-
filteredFields.map((field)=>({
85-
accessor: field.name||'',
86-
active: true,
87-
field: fieldasClientField,
88-
Heading: getTranslation(field?.label||(field.nameasstring),i18n),
89-
renderedCells: data.docs.map((doc: Record<string,unknown>)=>{
90-
if(!field.name||!doc[field.name]){
91-
returnnull
92-
}
93-
if(typeofdoc[field.name]==='object'){
94-
returnJSON.stringify(doc[field.name])
95-
}
96-
returnString(doc[field.name])
97-
}),
98-
}))asColumn[],
99-
)
100-
setDataToRender(data.docs)
101-
}
128+
129+
returnJSON.stringify(val)
130+
}),
131+
}))
132+
133+
setColumns(newColumns)
134+
setDataToRender(docs)
102135
}catch(error){
103-
console.error('Error fetching data:',error)
136+
console.error('Error fetching preview data:',error)
104137
}
105138
}
106139

107140
voidfetchData()
108-
},[collectionConfig?.fields,collectionSlug,draft,fields,limit,sort,where])
141+
},[collectionConfig,collectionSlug,draft,fields,i18n,limit,locale,sort,where])
109142

110143
return(
111144
<divclassName={baseClass}>
@@ -125,7 +158,12 @@ export const Preview = () => {
125158
/>
126159
)}
127160
</div>
128-
{dataToRender&&<Tablecolumns={columns}data={dataToRender}/>}
161+
{dataToRender&&
162+
(isCSV ? (
163+
<Tablecolumns={columns}data={dataToRender}/>
164+
) : (
165+
<CodeEditorLazylanguage="json"readOnlyvalue={JSON.stringify(dataToRender,null,2)}/>
166+
))}
129167
</div>
130168
)
131169
}

‎packages/plugin-import-export/src/index.ts‎

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
importtype{Config}from'payload'
1+
importtype{Config,FlattenedField}from'payload'
22

3-
import{deepMergeSimple}from'payload'
3+
import{addDataAndFileToRequest,deepMergeSimple}from'payload'
44

55
importtype{PluginDefaultTranslationsObject}from'./translations/types.js'
66
importtype{ImportExportPluginConfig,ToCSVFunction}from'./types.js'
77

8+
import{flattenObject}from'./export/flattenObject.js'
89
import{getCreateCollectionExportTask}from'./export/getCreateExportCollectionTask.js'
10+
import{getCustomFieldFunctions}from'./export/getCustomFieldFunctions.js'
11+
import{getSelect}from'./export/getSelect.js'
912
import{getExportCollection}from'./getExportCollection.js'
1013
import{translations}from'./translations/index.js'
1114

@@ -63,6 +66,69 @@ export const importExportPlugin =
6366

6467
// config.i18n.translations = deepMergeSimple(translations, config.i18n?.translations ?? {})
6568

69+
// Inject custom REST endpoints into the config
70+
config.endpoints=config.endpoints||[]
71+
config.endpoints.push({
72+
handler: async(req)=>{
73+
awaitaddDataAndFileToRequest(req)
74+
75+
const{ collectionSlug, draft, fields, limit, locale, sort, where }=req.dataas{
76+
collectionSlug: string
77+
draft?: 'no'|'yes'
78+
fields?: string[]
79+
limit?: number
80+
locale?: string
81+
sort?: any
82+
where?: any
83+
}
84+
85+
constcollection=req.payload.collections[collectionSlug]
86+
if(!collection){
87+
returnResponse.json(
88+
{error: `Collection with slug ${collectionSlug} not found`},
89+
{status: 400},
90+
)
91+
}
92+
93+
constselect=Array.isArray(fields)&&fields.length>0 ? getSelect(fields) : undefined
94+
95+
constresult=awaitreq.payload.find({
96+
collection: collectionSlug,
97+
depth: 1,
98+
draft: draft==='yes',
99+
limit: limit&&limit>10 ? 10 : limit,
100+
locale,
101+
overrideAccess: false,
102+
req,
103+
select,
104+
sort,
105+
where,
106+
})
107+
108+
constdocs=result.docs
109+
110+
consttoCSVFunctions=getCustomFieldFunctions({
111+
fields: collection.config.fieldsasFlattenedField[],
112+
select,
113+
})
114+
115+
consttransformed=docs.map((doc)=>
116+
flattenObject({
117+
doc,
118+
fields,
119+
toCSVFunctions,
120+
}),
121+
)
122+
123+
returnResponse.json({
124+
docs: transformed,
125+
totalDocs: result.totalDocs,
126+
})
127+
},
128+
method: 'post',
129+
path: '/preview-data',
130+
})
131+
66132
/**
67133
* Merge plugin translations
68134
*/

0 commit comments

Comments
 (0)