Skip to content

Commit 335af1b

Browse files
authored
fix(plugin-import-export): preview table to include all selected columns regardless of populated data (#12985)
### What? Ensure the export preview table includes all field keys as columns, even if those fields are not populated in any of the returned documents. ### Why? Previously, if none of the documents in the preview result had a value for a given field, that column would be missing entirely from the preview table. ### How? - Introduced a `getFlattenedFieldKeys` utility that recursively extracts all missing flattened field accessors from the collection’s config that are undefined - Updates the preview UI logic to build columns from all flattened keys, not just the first document
1 parent 583a733 commit 335af1b

File tree

4 files changed

+111
-13
lines changed

4 files changed

+111
-13
lines changed

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

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,12 @@ export const Preview = () => {
7474
return
7575
}
7676

77-
const { docs, totalDocs } = await res.json()
77+
const { docs, totalDocs }: { docs: Record<string, unknown>[]; totalDocs: number } =
78+
await res.json()
7879

7980
setResultCount(limit && limit < totalDocs ? limit : totalDocs)
8081

81-
const allKeys = Object.keys(docs[0] || {})
82+
const allKeys = Array.from(new Set(docs.flatMap((doc) => Object.keys(doc))))
8283
const defaultMetaFields = ['createdAt', 'updatedAt', '_status', 'id']
8384

8485
// Match CSV column ordering by building keys based on fields and regex
@@ -96,13 +97,10 @@ export const Preview = () => {
9697
})
9798
: allKeys.filter((key) => !defaultMetaFields.includes(key))
9899

99-
const includedMeta = new Set(selectedKeys)
100-
const missingMetaFields = defaultMetaFields.flatMap((field) => {
101-
const regex = fieldToRegex(field)
102-
return allKeys.filter((key) => regex.test(key) && !includedMeta.has(key))
103-
})
104-
105-
const fieldKeys = [...selectedKeys, ...missingMetaFields]
100+
const fieldKeys =
101+
Array.isArray(fields) && fields.length > 0
102+
? selectedKeys // strictly only what was selected
103+
: [...selectedKeys, ...defaultMetaFields.filter((key) => allKeys.includes(key))]
106104

107105
// Build columns based on flattened keys
108106
const newColumns: Column[] = fieldKeys.map((key) => ({

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { getCustomFieldFunctions } from './export/getCustomFieldFunctions.js'
1111
import { getSelect } from './export/getSelect.js'
1212
import { getExportCollection } from './getExportCollection.js'
1313
import { translations } from './translations/index.js'
14+
import { getFlattenedFieldKeys } from './utilities/getFlattenedFieldKeys.js'
1415

1516
export const importExportPlugin =
1617
(pluginConfig: ImportExportPluginConfig) =>
@@ -112,13 +113,23 @@ export const importExportPlugin =
112113
select,
113114
})
114115

115-
const transformed = docs.map((doc) =>
116-
flattenObject({
116+
const possibleKeys = getFlattenedFieldKeys(collection.config.fields as FlattenedField[])
117+
118+
const transformed = docs.map((doc) => {
119+
const row = flattenObject({
117120
doc,
118121
fields,
119122
toCSVFunctions,
120-
}),
121-
)
123+
})
124+
125+
for (const key of possibleKeys) {
126+
if (!(key in row)) {
127+
row[key] = null
128+
}
129+
}
130+
131+
return row
132+
})
122133

123134
return Response.json({
124135
docs: transformed,
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { type FlattenedField } from 'payload'
2+
3+
type FieldWithPresentational =
4+
| {
5+
fields?: FlattenedField[]
6+
name?: string
7+
tabs?: {
8+
fields: FlattenedField[]
9+
name?: string
10+
}[]
11+
type: 'collapsible' | 'row' | 'tabs'
12+
}
13+
| FlattenedField
14+
15+
export const getFlattenedFieldKeys = (fields: FieldWithPresentational[], prefix = ''): string[] => {
16+
const keys: string[] = []
17+
18+
fields.forEach((field) => {
19+
if (!('name' in field) || typeof field.name !== 'string') {
20+
return
21+
}
22+
23+
const name = prefix ? `${prefix}_${field.name}` : field.name
24+
25+
switch (field.type) {
26+
case 'array': {
27+
const subKeys = getFlattenedFieldKeys(field.fields as FlattenedField[], `${name}_0`)
28+
keys.push(...subKeys)
29+
break
30+
}
31+
case 'blocks':
32+
field.blocks.forEach((block) => {
33+
const blockKeys = getFlattenedFieldKeys(block.fields as FlattenedField[], `${name}_0`)
34+
keys.push(...blockKeys)
35+
})
36+
break
37+
case 'collapsible':
38+
case 'group':
39+
case 'row':
40+
keys.push(...getFlattenedFieldKeys(field.fields as FlattenedField[], name))
41+
break
42+
case 'relationship':
43+
if (field.hasMany) {
44+
// e.g. hasManyPolymorphic_0_value_id
45+
keys.push(`${name}_0_relationTo`, `${name}_0_value_id`)
46+
} else {
47+
// e.g. hasOnePolymorphic_id
48+
keys.push(`${name}_id`, `${name}_relationTo`)
49+
}
50+
break
51+
case 'tabs':
52+
if (field.tabs) {
53+
field.tabs.forEach((tab) => {
54+
if (tab.name) {
55+
const tabPrefix = prefix ? `${prefix}_${tab.name}` : tab.name
56+
keys.push(...getFlattenedFieldKeys(tab.fields, tabPrefix))
57+
} else {
58+
keys.push(...getFlattenedFieldKeys(tab.fields, prefix))
59+
}
60+
})
61+
}
62+
break
63+
default:
64+
if ('hasMany' in field && field.hasMany) {
65+
// Push placeholder for first index
66+
keys.push(`${name}_0`)
67+
} else {
68+
keys.push(name)
69+
}
70+
break
71+
}
72+
})
73+
74+
return keys
75+
}

test/plugin-import-export/payload-types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,13 @@ export interface User {
143143
hash?: string | null;
144144
loginAttempts?: number | null;
145145
lockUntil?: string | null;
146+
sessions?:
147+
| {
148+
id: string;
149+
createdAt?: string | null;
150+
expiresAt: string;
151+
}[]
152+
| null;
146153
password?: string | null;
147154
}
148155
/**
@@ -502,6 +509,13 @@ export interface UsersSelect<T extends boolean = true> {
502509
hash?: T;
503510
loginAttempts?: T;
504511
lockUntil?: T;
512+
sessions?:
513+
| T
514+
| {
515+
id?: T;
516+
createdAt?: T;
517+
expiresAt?: T;
518+
};
505519
}
506520
/**
507521
* This interface was referenced by `Config`'s JSON-Schema

0 commit comments

Comments
 (0)