fix: select with unnamed tabs (#8966)

### What?
Fixes `select` handling for properties inside of unnamed tabs using the
mongodb adapter.
Additionally, refactors `traverseFields` in drizzle to reuse logic from
groups / collapsible or rows if unnamed.

### Why?
`select` must work for any fields.

### How?
Fixes the `'tab'` case in `buildProjectionFromSelect` to handle when the
field is an unnamed tab.
Adds extra tests for named tabs / unnamed.
This commit is contained in:
Sasha
2024-10-31 18:06:05 +02:00
committed by GitHub
parent 090831c92c
commit 3175541c80
5 changed files with 151 additions and 52 deletions

View File

@@ -6,7 +6,7 @@ import {
type SelectType,
type TabAsField,
} from 'payload'
import { fieldAffectsData, getSelectMode } from 'payload/shared'
import { fieldAffectsData, getSelectMode, tabHasName } from 'payload/shared'
import type { MongooseAdapter } from '../index.js'
@@ -131,9 +131,17 @@ const traverseFields = ({
case 'group':
case 'tab':
case 'array':
case 'array': {
let fieldSelect: SelectType
if (field.type === 'tab' && !tabHasName(field)) {
fieldSelect = select
} else {
fieldSelect = select[field.name] as SelectType
}
if (field.type === 'array' && selectMode === 'include') {
select[field.name]['id'] = true
fieldSelect['id'] = true
}
traverseFields({
@@ -141,12 +149,13 @@ const traverseFields = ({
databaseSchemaPath: fieldDatabaseSchemaPath,
fields: field.fields,
projection,
select: select[field.name] as SelectType,
select: fieldSelect,
selectMode,
withinLocalizedField: fieldWithinLocalizedField,
})
break
}
case 'blocks': {
const blocksSelect = select[field.name] as SelectType

View File

@@ -1,5 +1,5 @@
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
import type { Field, JoinQuery, SelectMode, SelectType } from 'payload'
import type { Field, JoinQuery, SelectMode, SelectType, TabAsField } from 'payload'
import { and, eq, sql } from 'drizzle-orm'
import { fieldAffectsData, fieldIsVirtual, tabHasName } from 'payload/shared'
@@ -17,7 +17,7 @@ type TraverseFieldArgs = {
currentArgs: Result
currentTableName: string
depth?: number
fields: Field[]
fields: (Field | TabAsField)[]
joinQuery: JoinQuery
joins?: BuildQueryJoinAliases
locale?: string
@@ -77,7 +77,11 @@ export const traverseFields = ({
}
}
if (field.type === 'collapsible' || field.type === 'row') {
if (
field.type === 'collapsible' ||
field.type === 'row' ||
(field.type === 'tab' && !tabHasName(field))
) {
traverseFields({
_locales,
adapter,
@@ -100,43 +104,24 @@ export const traverseFields = ({
}
if (field.type === 'tabs') {
field.tabs.forEach((tab) => {
const tabPath = tabHasName(tab) ? `${path}${tab.name}_` : path
const tabTablePath = tabHasName(tab) ? `${tablePath}${toSnakeCase(tab.name)}_` : tablePath
const tabSelect = tabHasName(tab) ? select?.[tab.name] : select
if (tabSelect === false) {
return
}
let tabSelectAllOnCurrentLevel = selectAllOnCurrentLevel
if (tabHasName(tab) && select && !tabSelectAllOnCurrentLevel) {
tabSelectAllOnCurrentLevel =
select[tab.name] === true ||
(selectMode === 'exclude' && typeof select[tab.name] === 'undefined')
}
traverseFields({
_locales,
adapter,
currentArgs,
currentTableName,
depth,
fields: tab.fields,
joinQuery,
joins,
path: tabPath,
select: typeof tabSelect === 'object' ? tabSelect : undefined,
selectAllOnCurrentLevel: tabSelectAllOnCurrentLevel,
selectMode,
tablePath: tabTablePath,
topLevelArgs,
topLevelTableName,
versions,
withTabledFields,
})
traverseFields({
_locales,
adapter,
currentArgs,
currentTableName,
depth,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
joinQuery,
joins,
path,
select,
selectAllOnCurrentLevel,
selectMode,
tablePath,
topLevelArgs,
topLevelTableName,
versions,
withTabledFields,
})
return
@@ -369,10 +354,11 @@ export const traverseFields = ({
break
}
case 'group': {
const groupSelect = select?.[field.name]
case 'group':
case 'tab': {
const fieldSelect = select?.[field.name]
if (groupSelect === false) {
if (fieldSelect === false) {
break
}
@@ -386,11 +372,11 @@ export const traverseFields = ({
joinQuery,
joins,
path: `${path}${field.name}_`,
select: typeof groupSelect === 'object' ? groupSelect : undefined,
select: typeof fieldSelect === 'object' ? fieldSelect : undefined,
selectAllOnCurrentLevel:
selectAllOnCurrentLevel ||
groupSelect === true ||
(selectMode === 'exclude' && typeof groupSelect === 'undefined'),
fieldSelect === true ||
(selectMode === 'exclude' && typeof fieldSelect === 'undefined'),
selectMode,
tablePath: `${tablePath}${toSnakeCase(field.name)}_`,
topLevelArgs,

View File

@@ -74,5 +74,36 @@ export const PostsCollection: CollectionConfig = {
},
],
},
{
type: 'tabs',
tabs: [
{
name: 'tab',
fields: [
{
type: 'text',
name: 'text',
},
{
type: 'number',
name: 'number',
},
],
},
{
label: 'Tab Unnamed',
fields: [
{
type: 'text',
name: 'unnamedTabText',
},
{
type: 'number',
name: 'unnamedTabNumber',
},
],
},
],
},
],
}

View File

@@ -129,6 +129,55 @@ describe('Select', () => {
})
})
it('should select all the fields inside of named tab', async () => {
const res = await payload.findByID({
collection: 'posts',
id: postId,
select: {
tab: true,
},
})
expect(res).toStrictEqual({
id: postId,
tab: post.tab,
})
})
it('should select text field inside of named tab', async () => {
const res = await payload.findByID({
collection: 'posts',
id: postId,
select: {
tab: {
text: true,
},
},
})
expect(res).toStrictEqual({
id: postId,
tab: {
text: post.tab.text,
},
})
})
it('should select text field inside of unnamed tab', async () => {
const res = await payload.findByID({
collection: 'posts',
id: postId,
select: {
unnamedTabText: true,
},
})
expect(res).toStrictEqual({
id: postId,
unnamedTabText: post.unnamedTabText,
})
})
it('should select id as default from array', async () => {
const res = await payload.findByID({
collection: 'posts',
@@ -1662,7 +1711,7 @@ describe('Select', () => {
expect(richTextSlateRel.value).toStrictEqual(expectedHomePage)
})
it('REST API - should populate with the defaultPopulate select shape', async () => {
it('rEST API - should populate with the defaultPopulate select shape', async () => {
const restResult = await (
await restClient.GET(`/pages/${aboutPage.id}`, { query: { depth: 1 } })
).json()
@@ -1728,6 +1777,12 @@ function createPost() {
number: 1,
},
],
tab: {
text: 'text',
number: 1,
},
unnamedTabNumber: 2,
unnamedTabText: 'text2',
},
})
}

View File

@@ -45,6 +45,10 @@ export interface Config {
user: User & {
collection: 'users';
};
jobs?: {
tasks: unknown;
workflows?: unknown;
};
}
export interface UserAuthOperations {
forgotPassword: {
@@ -101,6 +105,12 @@ export interface Post {
}
)[]
| null;
tab?: {
text?: string | null;
number?: number | null;
};
unnamedTabText?: string | null;
unnamedTabNumber?: number | null;
updatedAt: string;
createdAt: string;
}
@@ -424,6 +434,14 @@ export interface PostsSelect<T extends boolean = true> {
blockName?: T;
};
};
tab?:
| T
| {
text?: T;
number?: T;
};
unnamedTabText?: T;
unnamedTabNumber?: T;
updatedAt?: T;
createdAt?: T;
}