From a53c1d551775c2c81b3708c7f3c29e6b9c044c47 Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Tue, 3 Dec 2024 21:22:28 -0500 Subject: [PATCH] fix: hidden and disabled fields cause incorrect field paths (#9680) --- docs/admin/fields.mdx | 2 +- .../Version/RenderFieldsToDiff/index.tsx | 4 +- .../payload/src/collections/config/client.ts | 1 - packages/payload/src/exports/shared.ts | 2 + packages/payload/src/fields/config/client.ts | 24 ++--- packages/payload/src/fields/config/types.ts | 15 +++ packages/payload/src/index.ts | 1 + .../src/utilities/fieldSchemaToJSON.ts | 8 +- .../ui/src/elements/FieldSelect/index.tsx | 4 +- .../src/elements/Table/DefaultCell/index.tsx | 4 +- .../TableColumns/buildColumnState.tsx | 13 ++- .../WhereBuilder/reduceClientFields.tsx | 5 +- packages/ui/src/forms/RenderFields/index.tsx | 4 +- .../addFieldStatePromise.ts | 18 ++-- .../fieldSchemasToFormState/renderField.tsx | 6 ++ packages/ui/src/utilities/buildTableState.ts | 1 + packages/ui/src/utilities/formatFields.ts | 4 +- packages/ui/src/utilities/renderTable.tsx | 6 ++ test/admin/payload-types.ts | 47 +++++++++ test/fields/collections/Text/e2e.spec.ts | 97 +++++++++++++++++++ test/fields/collections/Text/index.ts | 30 ++++++ test/fields/components/CustomField.tsx | 9 ++ test/fields/e2e.spec.ts | 1 + test/fields/payload-types.ts | 6 ++ test/helpers/e2e/toggleColumn.ts | 40 ++++++-- tsconfig.json | 2 +- 26 files changed, 303 insertions(+), 51 deletions(-) create mode 100644 test/fields/components/CustomField.tsx diff --git a/docs/admin/fields.mdx b/docs/admin/fields.mdx index fa0bf3c95..d436781ce 100644 --- a/docs/admin/fields.mdx +++ b/docs/admin/fields.mdx @@ -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. | diff --git a/packages/next/src/views/Version/RenderFieldsToDiff/index.tsx b/packages/next/src/views/Version/RenderFieldsToDiff/index.tsx index 340dcdf64..6b89ff0ac 100644 --- a/packages/next/src/views/Version/RenderFieldsToDiff/index.tsx +++ b/packages/next/src/views/Version/RenderFieldsToDiff/index.tsx @@ -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 = ({ return (
{fields?.map((field, i) => { - if ('name' in field && field.name === 'id') { + if (fieldIsID(field)) { return null } diff --git a/packages/payload/src/collections/config/client.ts b/packages/payload/src/collections/config/client.ts index 83316dce0..30f81c1cf 100644 --- a/packages/payload/src/collections/config/client.ts +++ b/packages/payload/src/collections/config/client.ts @@ -234,7 +234,6 @@ export const createClientCollectionConfig = ({ } break - break default: clientCollection[key] = collection[key] } diff --git a/packages/payload/src/exports/shared.ts b/packages/payload/src/exports/shared.ts index 29f7d6c12..741e19cd9 100644 --- a/packages/payload/src/exports/shared.ts +++ b/packages/payload/src/exports/shared.ts @@ -19,6 +19,8 @@ export { fieldIsArrayType, fieldIsBlockType, fieldIsGroupType, + fieldIsHiddenOrDisabled, + fieldIsID, fieldIsLocalized, fieldIsPresentationalOnly, fieldIsSidebar, diff --git a/packages/payload/src/fields/config/client.ts b/packages/payload/src/fields/config/client.ts index 0f7ab993d..b8f4140a5 100644 --- a/packages/payload/src/fields/config/client.ts +++ b/packages/payload/src/fields/config/client.ts @@ -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') diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts index dafdaa858..7c9be468c 100644 --- a/packages/payload/src/fields/config/types.ts +++ b/packages/payload/src/fields/config/types.ts @@ -1709,6 +1709,21 @@ export function fieldIsSidebar( + 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, >( diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index 08426ed38..465f8e697 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -590,6 +590,7 @@ export class BasePayload { if (!fieldAffectsData(field)) { return } + if (field.name === 'id') { customIDType = field.type return true diff --git a/packages/payload/src/utilities/fieldSchemaToJSON.ts b/packages/payload/src/utilities/fieldSchemaToJSON.ts index 37511f8c9..3cc8da200 100644 --- a/packages/payload/src/utilities/fieldSchemaToJSON.ts +++ b/packages/payload/src/utilities/fieldSchemaToJSON.ts @@ -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 = [] diff --git a/packages/ui/src/elements/FieldSelect/index.tsx b/packages/ui/src/elements/FieldSelect/index.tsx index e867a3ed9..5bf5b3dd4 100644 --- a/packages/ui/src/elements/FieldSelect/index.tsx +++ b/packages/ui/src/elements/FieldSelect/index.tsx @@ -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 diff --git a/packages/ui/src/elements/Table/DefaultCell/index.tsx b/packages/ui/src/elements/Table/DefaultCell/index.tsx index 07385576b..3c67ee9c9 100644 --- a/packages/ui/src/elements/Table/DefaultCell/index.tsx +++ b/packages/ui/src/elements/Table/DefaultCell/index.tsx @@ -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 = (props) => { } } - if ('name' in field && field.name === 'id') { + if (fieldIsID(field)) { return ( { // 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, ) diff --git a/packages/ui/src/elements/WhereBuilder/reduceClientFields.tsx b/packages/ui/src/elements/WhereBuilder/reduceClientFields.tsx index d9265a90e..355f4bf99 100644 --- a/packages/ui/src/elements/WhereBuilder/reduceClientFields.tsx +++ b/packages/ui/src/elements/WhereBuilder/reduceClientFields.tsx @@ -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 }, []) } diff --git a/packages/ui/src/forms/RenderFields/index.tsx b/packages/ui/src/forms/RenderFields/index.tsx index 09bb11a77..4f1e1a87f 100644 --- a/packages/ui/src/forms/RenderFields/index.tsx +++ b/packages/ui/src/forms/RenderFields/index.tsx @@ -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 = (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 } diff --git a/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts b/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts index b0ddc8ca1..f4fdaa63a 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts +++ b/packages/ui/src/forms/fieldSchemasToFormState/addFieldStatePromise.ts @@ -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) diff --git a/packages/ui/src/forms/fieldSchemasToFormState/renderField.tsx b/packages/ui/src/forms/fieldSchemasToFormState/renderField.tsx index 94c6ec0f1..2b7d5cbff 100644 --- a/packages/ui/src/forms/fieldSchemasToFormState/renderField.tsx +++ b/packages/ui/src/forms/fieldSchemasToFormState/renderField.tsx @@ -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 = { customComponents: fieldState?.customComponents || {}, field: clientField, diff --git a/packages/ui/src/utilities/buildTableState.ts b/packages/ui/src/utilities/buildTableState.ts index c5d7c9140..3a78c8cb8 100644 --- a/packages/ui/src/utilities/buildTableState.ts +++ b/packages/ui/src/utilities/buildTableState.ts @@ -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') diff --git a/packages/ui/src/utilities/formatFields.ts b/packages/ui/src/utilities/formatFields.ts index e0f13e088..fc40bef1d 100644 --- a/packages/ui/src/utilities/formatFields.ts +++ b/packages/ui/src/utilities/formatFields.ts @@ -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 diff --git a/packages/ui/src/utilities/renderTable.tsx b/packages/ui/src/utilities/renderTable.tsx index 94327111b..99ccd3483 100644 --- a/packages/ui/src/utilities/renderTable.tsx +++ b/packages/ui/src/utilities/renderTable.tsx @@ -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 => fields.reduce( (acc, field) => { + if (fieldIsHiddenOrDisabled(field)) { + return acc + } + if ('name' in field && field.admin?.components?.Filter) { acc.set( field.name, diff --git a/test/admin/payload-types.ts b/test/admin/payload-types.ts index bb15b9395..98067ca37 100644 --- a/test/admin/payload-types.ts +++ b/test/admin/payload-types.ts @@ -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 { 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; } diff --git a/test/fields/collections/Text/e2e.spec.ts b/test/fields/collections/Text/e2e.spec.ts index c02998ab3..c9ae46c04 100644 --- a/test/fields/collections/Text/e2e.spec.ts +++ b/test/fields/collections/Text/e2e.spec.ts @@ -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('') }) diff --git a/test/fields/collections/Text/index.ts b/test/fields/collections/Text/index.ts index b299d8194..e718fd9f7 100644 --- a/test/fields/collections/Text/index.ts +++ b/test/fields/collections/Text/index.ts @@ -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', diff --git a/test/fields/components/CustomField.tsx b/test/fields/components/CustomField.tsx new file mode 100644 index 000000000..8cf37860a --- /dev/null +++ b/test/fields/components/CustomField.tsx @@ -0,0 +1,9 @@ +'use client' + +import type { TextFieldServerComponent } from 'payload' + +import React from 'react' + +export const CustomField: TextFieldServerComponent = ({ schemaPath }) => { + return
{schemaPath}
+} diff --git a/test/fields/e2e.spec.ts b/test/fields/e2e.spec.ts index 000375781..0a12e58a6 100644 --- a/test/fields/e2e.spec.ts +++ b/test/fields/e2e.spec.ts @@ -50,6 +50,7 @@ describe('fields', () => { await ensureCompilationIsDone({ page, serverURL }) }) + beforeEach(async () => { await reInitializeDB({ serverURL, diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts index b95b57478..8c19391ae 100644 --- a/test/fields/payload-types.ts +++ b/test/fields/payload-types.ts @@ -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 { */ export interface TextFieldsSelect { text?: T; + hiddenTextField?: T; + adminHiddenTextField?: T; + disabledTextField?: T; localizedText?: T; i18nText?: T; defaultString?: T; diff --git a/test/helpers/e2e/toggleColumn.ts b/test/helpers/e2e/toggleColumn.ts index bb6b39aba..0be1faf08 100644 --- a/test/helpers/e2e/toggleColumn.ts +++ b/test/helpers/e2e/toggleColumn.ts @@ -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 => { @@ -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 => { + 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 diff --git a/tsconfig.json b/tsconfig.json index 681199977..53226b2c4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -37,7 +37,7 @@ ], "paths": { "@payload-config": [ - "./test/live-preview/config.ts" + "./test/_community/config.ts" ], "@payloadcms/live-preview": [ "./packages/live-preview/src"