Skip to content

Commit 859849e

Browse files
akhrarovsaidpaulpopus
authored andcommitted
fix(plugin-import-export): hide invalid sortBy options (payloadcms#11676)
<!-- Thank you for the PR! Please go through the checklist below and make sure you've completed all the steps. Please review the [CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md) document in this repository if you haven't already. The following items will ensure that your PR is handled as smoothly as possible: - PR Title must follow conventional commits format. For example, `feat: my new feature`, `fix(plugin-seo): my fix`. - Minimal description explained as if explained to someone not immediately familiar with the code. - Provide before/after screenshots or code diffs if applicable. - Link any related issues/discussions from GitHub or Discord. - Add review comments if necessary to explain to the reviewer the logic behind a change ### What? ### Why? ### How? Fixes # --> ### What? This PR aims to prevent runtime 500-level errors when using `plugin-import-export` by omitting invalid options such as arrays and blocks from the `SortyBy` component. This PR also omits the `sort` when it is false-y. ### Why? To prevent errors and show valid sortable fields to sort by. ### How? Omitting the sort when it is false-y from the preview, and omitting invalid `sortBy` options. Demo (After): [Pages---Payload.webm](https://github.com/user-attachments/assets/fca07160-8d66-44dc-a19f-0b32a93050f2) --------- Co-authored-by: Paul Popus <paul@payloadcms.com>
1 parent 35c1c60 commit 859849e

3 files changed

Lines changed: 158 additions & 3 deletions

File tree

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import type { ClientField } from 'payload'
2+
3+
import { describe, expect, it } from 'vitest'
4+
5+
import { reduceFields } from './reduceFields.js'
6+
7+
const values = (result: ReturnType<typeof reduceFields>) => result.map((f) => f.value)
8+
9+
describe('reduceFields', () => {
10+
describe('excludeUnsortable', () => {
11+
it('should include array and blocks fields by default', () => {
12+
const fields: ClientField[] = [
13+
{ name: 'title', type: 'text' },
14+
{ name: 'items', type: 'array', fields: [{ name: 'text', type: 'text' }] },
15+
{ name: 'layout', type: 'blocks', blocks: [] },
16+
]
17+
18+
const result = values(reduceFields({ fields }))
19+
20+
expect(result).toContain('title')
21+
expect(result).toContain('items')
22+
expect(result).toContain('layout')
23+
})
24+
25+
it('should exclude array fields when excludeUnsortable is true', () => {
26+
const fields: ClientField[] = [
27+
{ name: 'title', type: 'text' },
28+
{ name: 'items', type: 'array', fields: [{ name: 'text', type: 'text' }] },
29+
]
30+
31+
const result = values(reduceFields({ excludeUnsortable: true, fields }))
32+
33+
expect(result).toContain('title')
34+
expect(result).not.toContain('items')
35+
})
36+
37+
it('should exclude blocks fields when excludeUnsortable is true', () => {
38+
const fields: ClientField[] = [
39+
{ name: 'title', type: 'text' },
40+
{ name: 'layout', type: 'blocks', blocks: [] },
41+
]
42+
43+
const result = values(reduceFields({ excludeUnsortable: true, fields }))
44+
45+
expect(result).toContain('title')
46+
expect(result).not.toContain('layout')
47+
})
48+
})
49+
50+
describe('disabledFields', () => {
51+
it('should include all fields when disabledFields is empty', () => {
52+
const fields: ClientField[] = [
53+
{ name: 'title', type: 'text' },
54+
{ name: 'slug', type: 'text' },
55+
]
56+
57+
const result = values(reduceFields({ disabledFields: [], fields }))
58+
59+
expect(result).toContain('title')
60+
expect(result).toContain('slug')
61+
})
62+
63+
it('should exclude a field whose path is in disabledFields', () => {
64+
const fields: ClientField[] = [
65+
{ name: 'title', type: 'text' },
66+
{ name: 'slug', type: 'text' },
67+
]
68+
69+
const result = values(reduceFields({ disabledFields: ['slug'], fields }))
70+
71+
expect(result).toContain('title')
72+
expect(result).not.toContain('slug')
73+
})
74+
75+
it('should exclude nested fields whose paths start with a disabled parent path', () => {
76+
const fields: ClientField[] = [
77+
{
78+
name: 'meta',
79+
type: 'group',
80+
fields: [
81+
{ name: 'title', type: 'text' },
82+
{ name: 'description', type: 'text' },
83+
],
84+
},
85+
]
86+
87+
const result = values(reduceFields({ disabledFields: ['meta.description'], fields }))
88+
89+
expect(result).toContain('meta.title')
90+
expect(result).not.toContain('meta.description')
91+
})
92+
})
93+
94+
describe('combined excludeUnsortable and disabledFields', () => {
95+
it('should apply both filters simultaneously', () => {
96+
const fields: ClientField[] = [
97+
{ name: 'title', type: 'text' },
98+
{ name: 'slug', type: 'text' },
99+
{ name: 'items', type: 'array', fields: [{ name: 'text', type: 'text' }] },
100+
]
101+
102+
const result = values(
103+
reduceFields({ disabledFields: ['slug'], excludeUnsortable: true, fields }),
104+
)
105+
106+
expect(result).toContain('title')
107+
expect(result).not.toContain('slug')
108+
expect(result).not.toContain('items')
109+
})
110+
})
111+
112+
describe('recursive propagation through group sub-fields', () => {
113+
it('should propagate excludeUnsortable into group sub-fields', () => {
114+
const fields: ClientField[] = [
115+
{
116+
name: 'meta',
117+
type: 'group',
118+
fields: [
119+
{ name: 'title', type: 'text' },
120+
{ name: 'tags', type: 'array', fields: [{ name: 'tag', type: 'text' }] },
121+
],
122+
},
123+
]
124+
125+
const result = values(reduceFields({ excludeUnsortable: true, fields }))
126+
127+
expect(result).toContain('meta.title')
128+
expect(result).not.toContain('meta.tags')
129+
})
130+
131+
it('should propagate disabledFields into group sub-fields', () => {
132+
const fields: ClientField[] = [
133+
{
134+
name: 'meta',
135+
type: 'group',
136+
fields: [
137+
{ name: 'title', type: 'text' },
138+
{ name: 'description', type: 'text' },
139+
],
140+
},
141+
]
142+
143+
const result = values(reduceFields({ disabledFields: ['meta.description'], fields }))
144+
145+
expect(result).toContain('meta.title')
146+
expect(result).not.toContain('meta.description')
147+
})
148+
})
149+
})

packages/plugin-import-export/src/components/FieldsToExport/reduceFields.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,13 @@ const combineLabel = ({
4444

4545
export const reduceFields = ({
4646
disabledFields = [],
47+
excludeUnsortable = false,
4748
fields,
4849
labelPrefix = null,
4950
path = '',
5051
}: {
5152
disabledFields?: string[]
53+
excludeUnsortable?: boolean
5254
fields: ClientField[]
5355
labelPrefix?: React.ReactNode
5456
path?: string
@@ -59,16 +61,19 @@ export const reduceFields = ({
5961

6062
return fields.reduce<{ id: string; label: React.ReactNode; value: string }[]>(
6163
(fieldsToUse, field) => {
64+
const isArrayOrBlocks = field.type === 'array' || field.type === 'blocks'
65+
6266
// escape for a variety of reasons, include ui fields as they have `name`.
63-
if (field.type === 'ui') {
67+
if (field.type === 'ui' || (excludeUnsortable && isArrayOrBlocks)) {
6468
return fieldsToUse
6569
}
6670

67-
if (!(field.type === 'array' || field.type === 'blocks') && fieldHasSubFields(field)) {
71+
if (!isArrayOrBlocks && fieldHasSubFields(field)) {
6872
return [
6973
...fieldsToUse,
7074
...reduceFields({
7175
disabledFields,
76+
excludeUnsortable,
7277
fields: field.fields,
7378
labelPrefix: combineLabel({ field, prefix: labelPrefix }),
7479
path: createNestedClientFieldPath(path, field),
@@ -90,6 +95,7 @@ export const reduceFields = ({
9095
...tabFields,
9196
...reduceFields({
9297
disabledFields,
98+
excludeUnsortable,
9399
fields: tab.fields,
94100
labelPrefix: isNamedTab
95101
? combineLabel({

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export const SortBy: SelectFieldClientComponent = (props) => {
4545

4646
const collectionConfig = getEntityConfig({ collectionSlug: collectionSlug ?? collection })
4747
const fieldOptions = useMemo(
48-
() => reduceFields({ fields: collectionConfig?.fields }),
48+
() => reduceFields({ excludeUnsortable: true, fields: collectionConfig?.fields }),
4949
[collectionConfig?.fields],
5050
)
5151

0 commit comments

Comments
 (0)