fix: hidden and disabled fields cause incorrect field paths (#9680)
This commit is contained in:
@@ -50,7 +50,7 @@ The following options are available:
|
||||
| **`style`** | [CSS Properties](https://developer.mozilla.org/en-US/docs/Web/CSS) to inject into the root element of the field. |
|
||||
| **`className`** | Attach a [CSS class attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/Class_selectors) to the root DOM element of a field. |
|
||||
| **`readOnly`** | Setting a field to `readOnly` has no effect on the API whatsoever but disables the admin component's editability to prevent editors from modifying the field's value. |
|
||||
| **`disabled`** | If a field is `disabled`, it is completely omitted from the [Admin Panel](../admin/overview). |
|
||||
| **`disabled`** | If a field is `disabled`, it is completely omitted from the [Admin Panel](../admin/overview) entirely. |
|
||||
| **`disableBulkEdit`** | Set `disableBulkEdit` to `true` to prevent fields from appearing in the select options when making edits for multiple documents. Defaults to `true` for UI fields. |
|
||||
| **`disableListColumn`** | Set `disableListColumn` to `true` to prevent fields from appearing in the list view column selector. |
|
||||
| **`disableListFilter`** | Set `disableListFilter` to `true` to prevent fields from appearing in the list view filter options. |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { DiffMethod } from 'react-diff-viewer-continued'
|
||||
|
||||
import { fieldAffectsData } from 'payload/shared'
|
||||
import { fieldAffectsData, fieldIsID } from 'payload/shared'
|
||||
import React from 'react'
|
||||
|
||||
import type { diffComponents as _diffComponents } from './fields/index.js'
|
||||
@@ -29,7 +29,7 @@ const RenderFieldsToDiff: React.FC<Props> = ({
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
{fields?.map((field, i) => {
|
||||
if ('name' in field && field.name === 'id') {
|
||||
if (fieldIsID(field)) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -234,7 +234,6 @@ export const createClientCollectionConfig = ({
|
||||
}
|
||||
break
|
||||
|
||||
break
|
||||
default:
|
||||
clientCollection[key] = collection[key]
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ export {
|
||||
fieldIsArrayType,
|
||||
fieldIsBlockType,
|
||||
fieldIsGroupType,
|
||||
fieldIsHiddenOrDisabled,
|
||||
fieldIsID,
|
||||
fieldIsLocalized,
|
||||
fieldIsPresentationalOnly,
|
||||
fieldIsSidebar,
|
||||
|
||||
@@ -75,28 +75,24 @@ export const createClientField = ({
|
||||
}): ClientField => {
|
||||
const clientField: ClientField = {} as ClientField
|
||||
|
||||
const isHidden = 'hidden' in incomingField && incomingField?.hidden
|
||||
const disabledFromAdmin =
|
||||
incomingField?.admin && 'disabled' in incomingField.admin && incomingField.admin.disabled
|
||||
|
||||
if (fieldAffectsData(incomingField) && (isHidden || disabledFromAdmin)) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (const key in incomingField) {
|
||||
if (serverOnlyFieldProperties.includes(key as any)) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case 'admin':
|
||||
if (!incomingField.admin) {
|
||||
break
|
||||
}
|
||||
|
||||
clientField.admin = {} as AdminClient
|
||||
|
||||
for (const adminKey in incomingField.admin) {
|
||||
if (serverOnlyFieldAdminProperties.includes(adminKey as any)) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch (adminKey) {
|
||||
case 'description':
|
||||
if ('description' in incomingField.admin) {
|
||||
@@ -107,16 +103,20 @@ export const createClientField = ({
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
default:
|
||||
clientField.admin[adminKey] = incomingField.admin[adminKey]
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case 'blocks':
|
||||
case 'fields':
|
||||
case 'tabs':
|
||||
// Skip - we handle sub-fields in the switch below
|
||||
break
|
||||
|
||||
case 'label':
|
||||
//@ts-expect-error - would need to type narrow
|
||||
if (typeof incomingField.label === 'function') {
|
||||
@@ -126,7 +126,9 @@ export const createClientField = ({
|
||||
//@ts-expect-error - would need to type narrow
|
||||
clientField.label = incomingField.label
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
default:
|
||||
clientField[key] = incomingField[key]
|
||||
}
|
||||
@@ -243,6 +245,7 @@ export const createClientField = ({
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'richText': {
|
||||
if (!incomingField?.editor) {
|
||||
throw new MissingEditorProp(incomingField) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
|
||||
@@ -269,6 +272,7 @@ export const createClientField = ({
|
||||
if (serverOnlyFieldProperties.includes(key as any)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (key === 'fields') {
|
||||
clientTab.fields = createClientFields({
|
||||
defaultIDType,
|
||||
@@ -320,9 +324,7 @@ export const createClientFields = ({
|
||||
importMap,
|
||||
})
|
||||
|
||||
if (clientField) {
|
||||
clientFields.push(clientField)
|
||||
}
|
||||
clientFields.push(clientField)
|
||||
}
|
||||
|
||||
const hasID = flattenTopLevelFields(fields).some((f) => fieldAffectsData(f) && f.name === 'id')
|
||||
|
||||
@@ -1709,6 +1709,21 @@ export function fieldIsSidebar<TField extends ClientField | Field | TabAsField |
|
||||
return 'admin' in field && 'position' in field.admin && field.admin.position === 'sidebar'
|
||||
}
|
||||
|
||||
export function fieldIsID<TField extends ClientField | Field>(
|
||||
field: TField,
|
||||
): field is { name: 'id' } & TField {
|
||||
return 'name' in field && field.name === 'id'
|
||||
}
|
||||
|
||||
export function fieldIsHiddenOrDisabled<
|
||||
TField extends ClientField | Field | TabAsField | TabAsFieldClient,
|
||||
>(field: TField): field is { admin: { hidden: true } } & TField {
|
||||
return (
|
||||
('hidden' in field && field.hidden) ||
|
||||
('admin' in field && 'disabled' in field.admin && field.admin.disabled)
|
||||
)
|
||||
}
|
||||
|
||||
export function fieldAffectsData<
|
||||
TField extends ClientField | Field | TabAsField | TabAsFieldClient,
|
||||
>(
|
||||
|
||||
@@ -590,6 +590,7 @@ export class BasePayload {
|
||||
if (!fieldAffectsData(field)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (field.name === 'id') {
|
||||
customIDType = field.type
|
||||
return true
|
||||
|
||||
@@ -52,11 +52,11 @@ export const fieldSchemaToJSON = (fields: ClientField[]): FieldSchemaJSON => {
|
||||
|
||||
break
|
||||
|
||||
case 'collapsible':
|
||||
|
||||
case 'collapsible': // eslint-disable no-fallthrough
|
||||
case 'row':
|
||||
result = result.concat(fieldSchemaToJSON(field.fields))
|
||||
break
|
||||
|
||||
case 'group':
|
||||
acc.push({
|
||||
name: field.name,
|
||||
@@ -66,8 +66,7 @@ export const fieldSchemaToJSON = (fields: ClientField[]): FieldSchemaJSON => {
|
||||
|
||||
break
|
||||
|
||||
case 'relationship':
|
||||
|
||||
case 'relationship': // eslint-disable no-fallthrough
|
||||
case 'upload':
|
||||
acc.push({
|
||||
name: field.name,
|
||||
@@ -77,6 +76,7 @@ export const fieldSchemaToJSON = (fields: ClientField[]): FieldSchemaJSON => {
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
case 'tabs': {
|
||||
let tabFields = []
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { ClientField, FieldWithPath, FormState } from 'payload'
|
||||
|
||||
import { fieldAffectsData, fieldHasSubFields } from 'payload/shared'
|
||||
import { fieldAffectsData, fieldHasSubFields, fieldIsHiddenOrDisabled } from 'payload/shared'
|
||||
import React, { Fragment, useState } from 'react'
|
||||
|
||||
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js'
|
||||
@@ -69,7 +69,7 @@ const reduceFields = ({
|
||||
(fieldAffectsData(field) || field.type === 'ui') &&
|
||||
(field.admin.disableBulkEdit ||
|
||||
field.unique ||
|
||||
field.admin.hidden ||
|
||||
fieldIsHiddenOrDisabled(field) ||
|
||||
('readOnly' in field && field.readOnly))
|
||||
) {
|
||||
return fieldsToUse
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { DefaultCellComponentProps, UploadFieldClient } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import LinkImport from 'next/link.js'
|
||||
import { fieldAffectsData } from 'payload/shared'
|
||||
import { fieldAffectsData, fieldIsID } from 'payload/shared'
|
||||
import React from 'react' // TODO: abstract this out to support all routers
|
||||
|
||||
import { useConfig } from '../../../providers/Config/index.js'
|
||||
@@ -77,7 +77,7 @@ export const DefaultCell: React.FC<DefaultCellComponentProps> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
if ('name' in field && field.name === 'id') {
|
||||
if (fieldIsID(field)) {
|
||||
return (
|
||||
<WrapElement {...wrapElementProps}>
|
||||
<CodeCell
|
||||
|
||||
@@ -11,7 +11,12 @@ import type {
|
||||
} from 'payload'
|
||||
|
||||
import { MissingEditorProp } from 'payload'
|
||||
import { deepCopyObjectSimple, fieldIsPresentationalOnly } from 'payload/shared'
|
||||
import {
|
||||
deepCopyObjectSimple,
|
||||
fieldIsHiddenOrDisabled,
|
||||
fieldIsID,
|
||||
fieldIsPresentationalOnly,
|
||||
} from 'payload/shared'
|
||||
import React from 'react'
|
||||
|
||||
import type { ColumnPreferences } from '../../providers/ListQuery/index.js'
|
||||
@@ -70,7 +75,7 @@ export const buildColumnState = (args: Args): Column[] => {
|
||||
// place the `ID` field first, if it exists
|
||||
// do the same for the `useAsTitle` field with precedence over the `ID` field
|
||||
// then sort the rest of the fields based on the `defaultColumns` or `columnPreferences`
|
||||
const idFieldIndex = sortedFieldMap?.findIndex((field) => 'name' in field && field.name === 'id')
|
||||
const idFieldIndex = sortedFieldMap?.findIndex((field) => fieldIsID(field))
|
||||
|
||||
if (idFieldIndex > -1) {
|
||||
const idField = sortedFieldMap.splice(idFieldIndex, 1)[0]
|
||||
@@ -117,6 +122,10 @@ export const buildColumnState = (args: Args): Column[] => {
|
||||
const activeColumnsIndices = []
|
||||
|
||||
const sorted: Column[] = sortedFieldMap?.reduce((acc, field, index) => {
|
||||
if (fieldIsHiddenOrDisabled(field) && !fieldIsID(field)) {
|
||||
return acc
|
||||
}
|
||||
|
||||
const _field = _sortedFieldMap.find(
|
||||
(f) => 'name' in field && 'name' in f && f.name === field.name,
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ClientTranslationKeys, I18nClient } from '@payloadcms/translations
|
||||
import type { ClientField } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { tabHasName } from 'payload/shared'
|
||||
import { fieldIsHiddenOrDisabled, fieldIsID, tabHasName } from 'payload/shared'
|
||||
|
||||
import type { FieldCondition } from './types.js'
|
||||
|
||||
@@ -29,7 +29,7 @@ export const reduceClientFields = ({
|
||||
pathPrefix,
|
||||
}: ReduceClientFieldsArgs): FieldCondition[] => {
|
||||
return fields.reduce((reduced, field) => {
|
||||
if (field.admin?.disableListFilter) {
|
||||
if (field.admin?.disableListFilter || (fieldIsHiddenOrDisabled(field) && !fieldIsID(field))) {
|
||||
return reduced
|
||||
}
|
||||
|
||||
@@ -163,7 +163,6 @@ export const reduceClientFields = ({
|
||||
reduced.push(formattedField)
|
||||
return reduced
|
||||
}
|
||||
|
||||
return reduced
|
||||
}, [])
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { getFieldPaths } from 'payload/shared'
|
||||
import { fieldIsHiddenOrDisabled, getFieldPaths } from 'payload/shared'
|
||||
import React from 'react'
|
||||
|
||||
import type { RenderFieldsProps } from './types.js'
|
||||
@@ -45,7 +45,7 @@ export const RenderFields: React.FC<RenderFieldsProps> = (props) => {
|
||||
{fields.map((field, i) => {
|
||||
// For sidebar fields in the main fields array, `field` will be `null`, and visa versa
|
||||
// This is to keep the order of the fields consistent and maintain the correct index paths for the main fields (i)
|
||||
if (!field || field?.admin?.disabled) {
|
||||
if (!field || fieldIsHiddenOrDisabled(field)) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
deepCopyObjectSimple,
|
||||
fieldAffectsData,
|
||||
fieldHasSubFields,
|
||||
fieldIsHiddenOrDisabled,
|
||||
fieldIsID,
|
||||
fieldIsSidebar,
|
||||
getFieldPaths,
|
||||
tabHasName,
|
||||
@@ -138,11 +140,9 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
|
||||
const requiresRender = renderAllFields || previousFormState?.[path]?.requiresRender
|
||||
|
||||
const isHiddenField = 'hidden' in field && field?.hidden
|
||||
const disabledFromAdmin = field?.admin && 'disabled' in field.admin && field.admin.disabled
|
||||
|
||||
let fieldPermissions: SanitizedFieldPermissions = true
|
||||
if (fieldAffectsData(field) && !(isHiddenField || disabledFromAdmin)) {
|
||||
|
||||
if (fieldAffectsData(field) && !fieldIsHiddenOrDisabled(field)) {
|
||||
fieldPermissions =
|
||||
parentPermissions === true
|
||||
? parentPermissions
|
||||
@@ -233,7 +233,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
if (!omitParents && (!filter || filter(args))) {
|
||||
state[parentPath + '.id'] = {
|
||||
fieldSchema: includeSchema
|
||||
? field.fields.find((field) => 'name' in field && field.name === 'id')
|
||||
? field.fields.find((field) => fieldIsID(field))
|
||||
: undefined,
|
||||
initialValue: row.id,
|
||||
valid: true,
|
||||
@@ -343,9 +343,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
if (!omitParents && (!filter || filter(args))) {
|
||||
state[parentPath + '.id'] = {
|
||||
fieldSchema: includeSchema
|
||||
? block.fields.find(
|
||||
(blockField) => 'name' in blockField && blockField.name === 'id',
|
||||
)
|
||||
? block.fields.find((blockField) => fieldIsID(blockField))
|
||||
: undefined,
|
||||
initialValue: row.id,
|
||||
valid: true,
|
||||
@@ -724,9 +722,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
||||
}
|
||||
}
|
||||
|
||||
const isDisabled = field?.admin && 'disabled' in field.admin && field.admin.disabled
|
||||
|
||||
if (requiresRender && !isDisabled && renderFieldFn) {
|
||||
if (requiresRender && renderFieldFn && !fieldIsHiddenOrDisabled(field)) {
|
||||
const fieldState = state[path]
|
||||
|
||||
const fieldConfig = fieldSchemaMap.get(schemaPath)
|
||||
|
||||
@@ -2,10 +2,12 @@ import type { ClientComponentProps, ClientField, FieldPaths, ServerComponentProp
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { createClientField, MissingEditorProp } from 'payload'
|
||||
import { fieldIsHiddenOrDisabled } from 'payload/shared'
|
||||
|
||||
import type { RenderFieldMethod } from './types.js'
|
||||
|
||||
import { RenderServerComponent } from '../../elements/RenderServerComponent/index.js'
|
||||
|
||||
// eslint-disable-next-line payload/no-imports-from-exports-dir -- need this to reference already existing bundle. Otherwise, bundle size increases., payload/no-imports-from-exports-dir
|
||||
import { FieldDescription } from '../../exports/client/index.js'
|
||||
|
||||
@@ -45,6 +47,10 @@ export const renderField: RenderFieldMethod = ({
|
||||
importMap: req.payload.importMap,
|
||||
})
|
||||
|
||||
if (fieldIsHiddenOrDisabled(clientField)) {
|
||||
return
|
||||
}
|
||||
|
||||
const clientProps: ClientComponentProps & Partial<FieldPaths> = {
|
||||
customComponents: fieldState?.customComponents || {},
|
||||
field: clientField,
|
||||
|
||||
@@ -100,6 +100,7 @@ export const buildTableState = async (
|
||||
if (!canAccessAdmin) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
|
||||
// Match the user collection to the global admin config
|
||||
} else if (adminUserSlug !== incomingUserSlug) {
|
||||
throw new Error('Unauthorized')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Field } from 'payload'
|
||||
|
||||
import { fieldAffectsData } from 'payload/shared'
|
||||
import { fieldAffectsData, fieldIsID } from 'payload/shared'
|
||||
|
||||
export const formatFields = (fields: Field[], isEditing?: boolean): Field[] =>
|
||||
isEditing ? fields.filter((field) => !fieldAffectsData(field) || field.name !== 'id') : fields
|
||||
isEditing ? fields.filter((field) => !fieldAffectsData(field) || !fieldIsID(field)) : fields
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
} from 'payload'
|
||||
|
||||
import { getTranslation, type I18nClient } from '@payloadcms/translations'
|
||||
import { fieldIsHiddenOrDisabled, fieldIsID } from 'payload/shared'
|
||||
|
||||
// eslint-disable-next-line payload/no-imports-from-exports-dir
|
||||
import type { Column } from '../exports/client/index.js'
|
||||
@@ -17,6 +18,7 @@ import { RenderServerComponent } from '../elements/RenderServerComponent/index.j
|
||||
import { buildColumnState } from '../elements/TableColumns/buildColumnState.js'
|
||||
import { filterFields } from '../elements/TableColumns/filterFields.js'
|
||||
import { getInitialColumns } from '../elements/TableColumns/getInitialColumns.js'
|
||||
|
||||
// eslint-disable-next-line payload/no-imports-from-exports-dir
|
||||
import { Pill, Table } from '../exports/client/index.js'
|
||||
|
||||
@@ -26,6 +28,10 @@ export const renderFilters = (
|
||||
): Map<string, React.ReactNode> =>
|
||||
fields.reduce(
|
||||
(acc, field) => {
|
||||
if (fieldIsHiddenOrDisabled(field)) {
|
||||
return acc
|
||||
}
|
||||
|
||||
if ('name' in field && field.admin?.components?.Filter) {
|
||||
acc.set(
|
||||
field.name,
|
||||
|
||||
@@ -218,6 +218,27 @@ export interface CustomField {
|
||||
descriptionAsFunction?: string | null;
|
||||
descriptionAsComponent?: string | null;
|
||||
customSelectField?: string | null;
|
||||
relationshipFieldWithBeforeAfterInputs?: (string | null) | Post;
|
||||
arrayFieldWithBeforeAfterInputs?:
|
||||
| {
|
||||
someTextField?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
blocksFieldWithBeforeAfterInputs?:
|
||||
| {
|
||||
textField?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'blockFields';
|
||||
}[]
|
||||
| null;
|
||||
text?: string | null;
|
||||
groupFieldWithBeforeAfterInputs?: {
|
||||
textOne?: string | null;
|
||||
textTwo?: string | null;
|
||||
};
|
||||
radioFieldWithBeforeAfterInputs?: ('one' | 'two' | 'three') | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -538,6 +559,32 @@ export interface CustomFieldsSelect<T extends boolean = true> {
|
||||
descriptionAsFunction?: T;
|
||||
descriptionAsComponent?: T;
|
||||
customSelectField?: T;
|
||||
relationshipFieldWithBeforeAfterInputs?: T;
|
||||
arrayFieldWithBeforeAfterInputs?:
|
||||
| T
|
||||
| {
|
||||
someTextField?: T;
|
||||
id?: T;
|
||||
};
|
||||
blocksFieldWithBeforeAfterInputs?:
|
||||
| T
|
||||
| {
|
||||
blockFields?:
|
||||
| T
|
||||
| {
|
||||
textField?: T;
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
};
|
||||
};
|
||||
text?: T;
|
||||
groupFieldWithBeforeAfterInputs?:
|
||||
| T
|
||||
| {
|
||||
textOne?: T;
|
||||
textTwo?: T;
|
||||
};
|
||||
radioFieldWithBeforeAfterInputs?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { openListColumns, toggleColumn } from 'helpers/e2e/toggleColumn.js'
|
||||
import path from 'path'
|
||||
import { wait } from 'payload/shared'
|
||||
import { fileURLToPath } from 'url'
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
exactText,
|
||||
initPageConsoleErrorCatch,
|
||||
saveDocAndAssert,
|
||||
selectTableRow,
|
||||
} from '../../../helpers.js'
|
||||
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
||||
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
|
||||
@@ -67,6 +69,93 @@ describe('Text', () => {
|
||||
await ensureCompilationIsDone({ page, serverURL })
|
||||
})
|
||||
|
||||
describe('hidden and disabled fields', () => {
|
||||
test('should not render top-level hidden fields in the UI', async () => {
|
||||
await page.goto(url.create)
|
||||
await expect(page.locator('#field-hiddenTextField')).toBeHidden()
|
||||
await page.goto(url.list)
|
||||
await expect(page.locator('.cell-hiddenTextField')).toBeHidden()
|
||||
await expect(page.locator('#heading-hiddenTextField')).toBeHidden()
|
||||
|
||||
const columnContainer = await openListColumns(page, {})
|
||||
|
||||
await expect(
|
||||
columnContainer.locator('.column-selector__column', {
|
||||
hasText: exactText('Hidden Text Field'),
|
||||
}),
|
||||
).toBeHidden()
|
||||
|
||||
await selectTableRow(page, 'Seeded text document')
|
||||
await page.locator('.edit-many__toggle').click()
|
||||
await page.locator('.field-select .rs__control').click()
|
||||
|
||||
const hiddenFieldOption = page.locator('.rs__option', {
|
||||
hasText: exactText('Hidden Text Field'),
|
||||
})
|
||||
|
||||
await expect(hiddenFieldOption).toBeHidden()
|
||||
})
|
||||
|
||||
test('should not show disabled fields in the UI', async () => {
|
||||
await page.goto(url.create)
|
||||
await expect(page.locator('#field-disabledTextField')).toHaveCount(0)
|
||||
await page.goto(url.list)
|
||||
await expect(page.locator('.cell-disabledTextField')).toBeHidden()
|
||||
await expect(page.locator('#heading-disabledTextField')).toBeHidden()
|
||||
|
||||
const columnContainer = await openListColumns(page, {})
|
||||
|
||||
await expect(
|
||||
columnContainer.locator('.column-selector__column', {
|
||||
hasText: exactText('Disabled Text Field'),
|
||||
}),
|
||||
).toBeHidden()
|
||||
|
||||
await selectTableRow(page, 'Seeded text document')
|
||||
|
||||
await page.locator('.edit-many__toggle').click()
|
||||
|
||||
await page.locator('.field-select .rs__control').click()
|
||||
|
||||
const disabledFieldOption = page.locator('.rs__option', {
|
||||
hasText: exactText('Disabled Text Field'),
|
||||
})
|
||||
|
||||
await expect(disabledFieldOption).toBeHidden()
|
||||
})
|
||||
|
||||
test('should render hidden input for admin.hidden fields', async () => {
|
||||
await page.goto(url.create)
|
||||
await expect(page.locator('#field-adminHiddenTextField')).toHaveAttribute('type', 'hidden')
|
||||
await page.goto(url.list)
|
||||
await expect(page.locator('.cell-adminHiddenTextField').first()).toBeVisible()
|
||||
await expect(page.locator('#heading-adminHiddenTextField')).toBeVisible()
|
||||
|
||||
const columnContainer = await openListColumns(page, {})
|
||||
|
||||
await expect(
|
||||
columnContainer.locator('.column-selector__column', {
|
||||
hasText: exactText('Admin Hidden Text Field'),
|
||||
}),
|
||||
).toBeVisible()
|
||||
|
||||
await selectTableRow(page, 'Seeded text document')
|
||||
await page.locator('.edit-many__toggle').click()
|
||||
await page.locator('.field-select .rs__control').click()
|
||||
|
||||
const adminHiddenFieldOption = page.locator('.rs__option', {
|
||||
hasText: exactText('Admin Hidden Text Field'),
|
||||
})
|
||||
|
||||
await expect(adminHiddenFieldOption).toBeVisible()
|
||||
})
|
||||
|
||||
test('hidden and disabled fields should not break subsequent field paths', async () => {
|
||||
await page.goto(url.create)
|
||||
await expect(page.locator('#custom-field-schema-path')).toHaveText('text-fields._index-4')
|
||||
})
|
||||
})
|
||||
|
||||
test('should display field in list view', async () => {
|
||||
await page.goto(url.list)
|
||||
const textCell = page.locator('.row-1 .cell-text')
|
||||
@@ -129,7 +218,15 @@ describe('Text', () => {
|
||||
|
||||
test('should display i18n label in cells when missing field data', async () => {
|
||||
await page.goto(url.list)
|
||||
await page.waitForURL(new RegExp(`${url.list}.*\\?.*`))
|
||||
|
||||
await toggleColumn(page, {
|
||||
targetState: 'on',
|
||||
columnLabel: 'Text en',
|
||||
})
|
||||
|
||||
const textCell = page.locator('.row-1 .cell-i18nText')
|
||||
|
||||
await expect(textCell).toHaveText('<No Text en>')
|
||||
})
|
||||
|
||||
|
||||
@@ -17,6 +17,36 @@ const TextFields: CollectionConfig = {
|
||||
beforeDuplicate: [({ value }) => `${value} - duplicate`],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'hiddenTextField',
|
||||
type: 'text',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
name: 'adminHiddenTextField',
|
||||
type: 'text',
|
||||
admin: {
|
||||
hidden: true,
|
||||
description: 'This field should be hidden',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'disabledTextField',
|
||||
type: 'text',
|
||||
admin: {
|
||||
disabled: true,
|
||||
description: 'This field should be disabled',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
admin: {
|
||||
components: {
|
||||
Field: './components/CustomField.tsx#CustomField',
|
||||
},
|
||||
},
|
||||
fields: [],
|
||||
},
|
||||
{
|
||||
name: 'localizedText',
|
||||
type: 'text',
|
||||
|
||||
9
test/fields/components/CustomField.tsx
Normal file
9
test/fields/components/CustomField.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import type { TextFieldServerComponent } from 'payload'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export const CustomField: TextFieldServerComponent = ({ schemaPath }) => {
|
||||
return <div id="custom-field-schema-path">{schemaPath}</div>
|
||||
}
|
||||
@@ -50,6 +50,7 @@ describe('fields', () => {
|
||||
|
||||
await ensureCompilationIsDone({ page, serverURL })
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await reInitializeDB({
|
||||
serverURL,
|
||||
|
||||
@@ -869,6 +869,9 @@ export interface BlockField {
|
||||
export interface TextField {
|
||||
id: string;
|
||||
text: string;
|
||||
hiddenTextField?: string | null;
|
||||
adminHiddenTextField?: string | null;
|
||||
disabledTextField?: string | null;
|
||||
localizedText?: string | null;
|
||||
i18nText?: string | null;
|
||||
defaultString?: string | null;
|
||||
@@ -3234,6 +3237,9 @@ export interface TabsFieldsSelect<T extends boolean = true> {
|
||||
*/
|
||||
export interface TextFieldsSelect<T extends boolean = true> {
|
||||
text?: T;
|
||||
hiddenTextField?: T;
|
||||
adminHiddenTextField?: T;
|
||||
disabledTextField?: T;
|
||||
localizedText?: T;
|
||||
i18nText?: T;
|
||||
defaultString?: T;
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
import { wait } from 'payload/shared'
|
||||
|
||||
import { exactText } from '../../helpers.js'
|
||||
|
||||
export const toggleColumn = async (
|
||||
export const openListColumns = async (
|
||||
page: Page,
|
||||
{
|
||||
togglerSelector = '.list-controls__toggle-columns',
|
||||
columnContainerSelector = '.list-controls__columns',
|
||||
columnLabel,
|
||||
}: {
|
||||
columnContainerSelector?: string
|
||||
columnLabel: string
|
||||
togglerSelector?: string
|
||||
},
|
||||
): Promise<any> => {
|
||||
@@ -25,6 +24,25 @@ export const toggleColumn = async (
|
||||
|
||||
await expect(page.locator(`${columnContainerSelector}.rah-static--height-auto`)).toBeVisible()
|
||||
|
||||
return columnContainer
|
||||
}
|
||||
|
||||
export const toggleColumn = async (
|
||||
page: Page,
|
||||
{
|
||||
togglerSelector,
|
||||
columnContainerSelector,
|
||||
columnLabel,
|
||||
targetState: targetStateFromArgs,
|
||||
}: {
|
||||
columnContainerSelector?: string
|
||||
columnLabel: string
|
||||
targetState?: 'off' | 'on'
|
||||
togglerSelector?: string
|
||||
},
|
||||
): Promise<any> => {
|
||||
const columnContainer = await openListColumns(page, { togglerSelector, columnContainerSelector })
|
||||
|
||||
const column = columnContainer.locator(`.column-selector .column-selector__column`, {
|
||||
hasText: exactText(columnLabel),
|
||||
})
|
||||
@@ -33,16 +51,24 @@ export const toggleColumn = async (
|
||||
el.classList.contains('column-selector__column--active'),
|
||||
)
|
||||
|
||||
const targetState =
|
||||
targetStateFromArgs !== undefined ? targetStateFromArgs : isActiveBeforeClick ? 'off' : 'on'
|
||||
|
||||
await expect(column).toBeVisible()
|
||||
|
||||
await column.click()
|
||||
if (
|
||||
(isActiveBeforeClick && targetState === 'off') ||
|
||||
(!isActiveBeforeClick && targetState === 'on')
|
||||
) {
|
||||
await column.click()
|
||||
}
|
||||
|
||||
if (isActiveBeforeClick) {
|
||||
if (targetState === 'off') {
|
||||
// no class
|
||||
await expect(column).not.toHaveClass('column-selector__column--active')
|
||||
await expect(column).not.toHaveClass(/column-selector__column--active/)
|
||||
} else {
|
||||
// has class
|
||||
await expect(column).toHaveClass('column-selector__column--active')
|
||||
await expect(column).toHaveClass(/column-selector__column--active/)
|
||||
}
|
||||
|
||||
return column
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
],
|
||||
"paths": {
|
||||
"@payload-config": [
|
||||
"./test/live-preview/config.ts"
|
||||
"./test/_community/config.ts"
|
||||
],
|
||||
"@payloadcms/live-preview": [
|
||||
"./packages/live-preview/src"
|
||||
|
||||
Reference in New Issue
Block a user