fix: admin.hidden not respected for RSCs, render-field server function not respecting field-overrides client-side (#13869)
This PR fixes 2 issues: - the `fieldConfig.admin.hidden` property had no effect for react server components, because the RSC was returned before we're checking for `admin.hidden` in RenderFields - the `render-field` server function did not propagate fieldConfig overrides to the clientProps. This means overriding `admin.Label` had no effect Adds e2e tests for both issues
This commit is contained in:
@@ -53,10 +53,6 @@ export function RenderField({
|
||||
}: RenderFieldProps) {
|
||||
const CustomField = useFormFields(([fields]) => fields && fields?.[path]?.customComponents?.Field)
|
||||
|
||||
if (CustomField !== undefined) {
|
||||
return CustomField || null
|
||||
}
|
||||
|
||||
const baseFieldProps: Pick<
|
||||
ClientComponentProps,
|
||||
'forceRender' | 'permissions' | 'readOnly' | 'schemaPath'
|
||||
@@ -67,6 +63,14 @@ export function RenderField({
|
||||
schemaPath,
|
||||
}
|
||||
|
||||
if (clientFieldConfig.admin?.hidden) {
|
||||
return <HiddenField {...baseFieldProps} path={path} />
|
||||
}
|
||||
|
||||
if (CustomField !== undefined) {
|
||||
return CustomField || null
|
||||
}
|
||||
|
||||
const iterableFieldProps = {
|
||||
...baseFieldProps,
|
||||
indexPath,
|
||||
@@ -74,10 +78,6 @@ export function RenderField({
|
||||
parentSchemaPath,
|
||||
}
|
||||
|
||||
if (clientFieldConfig.admin?.hidden) {
|
||||
return <HiddenField {...baseFieldProps} path={path} />
|
||||
}
|
||||
|
||||
switch (clientFieldConfig.type) {
|
||||
case 'array':
|
||||
return <ArrayField {...iterableFieldProps} field={clientFieldConfig} path={path} />
|
||||
|
||||
@@ -32,6 +32,7 @@ export const renderField: RenderFieldMethod = ({
|
||||
fieldConfig,
|
||||
fieldSchemaMap,
|
||||
fieldState,
|
||||
forceCreateClientField,
|
||||
formState,
|
||||
indexPath,
|
||||
lastRenderedPath,
|
||||
@@ -54,14 +55,15 @@ export const renderField: RenderFieldMethod = ({
|
||||
return
|
||||
}
|
||||
|
||||
const clientField = clientFieldSchemaMap
|
||||
? (clientFieldSchemaMap.get(schemaPath) as ClientField)
|
||||
: createClientField({
|
||||
defaultIDType: req.payload.config.db.defaultIDType,
|
||||
field: fieldConfig,
|
||||
i18n: req.i18n,
|
||||
importMap: req.payload.importMap,
|
||||
})
|
||||
const clientField =
|
||||
clientFieldSchemaMap && !forceCreateClientField
|
||||
? (clientFieldSchemaMap.get(schemaPath) as ClientField)
|
||||
: createClientField({
|
||||
defaultIDType: req.payload.config.db.defaultIDType,
|
||||
field: fieldConfig,
|
||||
i18n: req.i18n,
|
||||
importMap: req.payload.importMap,
|
||||
})
|
||||
|
||||
const clientProps: ClientComponentProps & Partial<FieldPaths> = {
|
||||
field: clientField,
|
||||
|
||||
@@ -108,6 +108,8 @@ export const _internal_renderFieldHandler: ServerFunction<
|
||||
preferences: {
|
||||
fields: {},
|
||||
},
|
||||
// If we are passed a field override, we want to ensure we create a new client field based on that override
|
||||
forceCreateClientField: fieldArg ? true : false,
|
||||
previousFieldState: undefined,
|
||||
renderAllFields: true,
|
||||
req,
|
||||
|
||||
@@ -18,6 +18,12 @@ export type RenderFieldArgs = {
|
||||
fieldConfig: Field
|
||||
fieldSchemaMap: FieldSchemaMap
|
||||
fieldState: FieldState
|
||||
/**
|
||||
* If set to true, it will force creating a clientField based on the passed fieldConfig instead of pulling
|
||||
* the client field from the clientFieldSchemaMap. This is useful if the passed fieldConfig differs from the one in the schema map,
|
||||
* e.g. when calling the render-field server function and passing a field config override.
|
||||
*/
|
||||
forceCreateClientField?: boolean
|
||||
formState: FormState
|
||||
id?: number | string
|
||||
indexPath: string
|
||||
|
||||
@@ -2,6 +2,7 @@ import { expect, test } from '@playwright/test'
|
||||
import { AdminUrlUtil } from 'helpers/adminUrlUtil.js'
|
||||
import { reInitializeDB } from 'helpers/reInitializeDB.js'
|
||||
import path from 'path'
|
||||
import { wait } from 'payload/shared'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { ensureCompilationIsDone, saveDocAndAssert } from '../../../helpers.js'
|
||||
@@ -102,5 +103,19 @@ describe('Lexical On Demand', () => {
|
||||
await expect(lexical.drawer.locator('#field-rows')).toHaveValue('5')
|
||||
await expect(lexical.drawer.locator('#field-columns')).toHaveValue('5')
|
||||
})
|
||||
|
||||
test('on-demand editor renders label', async ({ page }) => {
|
||||
await expect(page.locator('.field-label[for="field-myField"]')).toHaveText('My Label')
|
||||
})
|
||||
|
||||
test('ensure anchor richText field is hidden', async ({ page }) => {
|
||||
// Important: Wait for all fields to render
|
||||
await wait(1000)
|
||||
await expect(page.locator('.shimmer')).toHaveCount(0)
|
||||
|
||||
await expect(page.locator('.field-label[for="field-hiddenAnchor"]')).toHaveCount(0)
|
||||
|
||||
await expect(page.locator('.rich-text-lexical')).toHaveCount(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,8 +6,6 @@ import type { JSONFieldClientComponent } from 'payload'
|
||||
import { buildEditorState, RenderLexical } from '@payloadcms/richtext-lexical/client'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { lexicalFullyFeaturedSlug } from '../../slugs.js'
|
||||
|
||||
export const Component: JSONFieldClientComponent = () => {
|
||||
const [value, setValue] = useState<DefaultTypedEditorState | undefined>(() =>
|
||||
buildEditorState({ text: 'state default' }),
|
||||
@@ -21,9 +19,9 @@ export const Component: JSONFieldClientComponent = () => {
|
||||
<div>
|
||||
Default Component:
|
||||
<RenderLexical
|
||||
field={{ name: 'myField' }}
|
||||
field={{ name: 'myField', label: 'My Label' }}
|
||||
initialValue={buildEditorState({ text: 'defaultValue' })}
|
||||
schemaPath={`collection.${lexicalFullyFeaturedSlug}.richText`}
|
||||
schemaPath={`collection.OnDemandOutsideForm.hiddenAnchor`}
|
||||
setValue={setValue as any}
|
||||
value={value}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { EXPERIMENTAL_TableFeature, lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
|
||||
export const OnDemandOutsideForm: CollectionConfig = {
|
||||
slug: 'OnDemandOutsideForm',
|
||||
fields: [
|
||||
@@ -12,5 +14,15 @@ export const OnDemandOutsideForm: CollectionConfig = {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'hiddenAnchor',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({
|
||||
features: ({ rootFeatures }) => [...rootFeatures, EXPERIMENTAL_TableFeature()],
|
||||
}),
|
||||
admin: {
|
||||
hidden: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ export interface Config {
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
defaultIDType: number;
|
||||
};
|
||||
globals: {
|
||||
tabsWithRichText: TabsWithRichText;
|
||||
@@ -170,7 +170,7 @@ export interface UserAuthOperations {
|
||||
* via the `definition` "lexical-fully-featured".
|
||||
*/
|
||||
export interface LexicalFullyFeatured {
|
||||
id: string;
|
||||
id: number;
|
||||
richText?: {
|
||||
root: {
|
||||
type: string;
|
||||
@@ -194,7 +194,7 @@ export interface LexicalFullyFeatured {
|
||||
* via the `definition` "lexical-link-feature".
|
||||
*/
|
||||
export interface LexicalLinkFeature {
|
||||
id: string;
|
||||
id: number;
|
||||
richText?: {
|
||||
root: {
|
||||
type: string;
|
||||
@@ -242,7 +242,7 @@ export interface LexicalHeadingFeature {
|
||||
* via the `definition` "lexical-jsx-converter".
|
||||
*/
|
||||
export interface LexicalJsxConverter {
|
||||
id: string;
|
||||
id: number;
|
||||
richText?: {
|
||||
root: {
|
||||
type: string;
|
||||
@@ -266,7 +266,7 @@ export interface LexicalJsxConverter {
|
||||
* via the `definition` "lexical-fields".
|
||||
*/
|
||||
export interface LexicalField {
|
||||
id: string;
|
||||
id: number;
|
||||
title: string;
|
||||
lexicalRootEditor?: {
|
||||
root: {
|
||||
@@ -322,7 +322,7 @@ export interface LexicalField {
|
||||
* via the `definition` "lexical-migrate-fields".
|
||||
*/
|
||||
export interface LexicalMigrateField {
|
||||
id: string;
|
||||
id: number;
|
||||
title: string;
|
||||
lexicalWithLexicalPluginData?: {
|
||||
root: {
|
||||
@@ -417,7 +417,7 @@ export interface LexicalMigrateField {
|
||||
* via the `definition` "lexical-localized-fields".
|
||||
*/
|
||||
export interface LexicalLocalizedField {
|
||||
id: string;
|
||||
id: number;
|
||||
title: string;
|
||||
/**
|
||||
* Non-localized field with localized block subfields
|
||||
@@ -463,7 +463,7 @@ export interface LexicalLocalizedField {
|
||||
* via the `definition` "lexicalObjectReferenceBug".
|
||||
*/
|
||||
export interface LexicalObjectReferenceBug {
|
||||
id: string;
|
||||
id: number;
|
||||
lexicalDefault?: {
|
||||
root: {
|
||||
type: string;
|
||||
@@ -502,7 +502,7 @@ export interface LexicalObjectReferenceBug {
|
||||
* via the `definition` "LexicalInBlock".
|
||||
*/
|
||||
export interface LexicalInBlock {
|
||||
id: string;
|
||||
id: number;
|
||||
content?: {
|
||||
root: {
|
||||
type: string;
|
||||
@@ -548,7 +548,7 @@ export interface LexicalInBlock {
|
||||
* via the `definition` "lexical-access-control".
|
||||
*/
|
||||
export interface LexicalAccessControl {
|
||||
id: string;
|
||||
id: number;
|
||||
title?: string | null;
|
||||
richText?: {
|
||||
root: {
|
||||
@@ -573,7 +573,7 @@ export interface LexicalAccessControl {
|
||||
* via the `definition` "lexical-relationship-fields".
|
||||
*/
|
||||
export interface LexicalRelationshipField {
|
||||
id: string;
|
||||
id: number;
|
||||
richText?: {
|
||||
root: {
|
||||
type: string;
|
||||
@@ -612,7 +612,7 @@ export interface LexicalRelationshipField {
|
||||
* via the `definition` "rich-text-fields".
|
||||
*/
|
||||
export interface RichTextField {
|
||||
id: string;
|
||||
id: number;
|
||||
title: string;
|
||||
lexicalCustomFields: {
|
||||
root: {
|
||||
@@ -693,7 +693,7 @@ export interface RichTextField {
|
||||
* via the `definition` "text-fields".
|
||||
*/
|
||||
export interface TextField {
|
||||
id: string;
|
||||
id: number;
|
||||
text: string;
|
||||
hiddenTextField?: string | null;
|
||||
/**
|
||||
@@ -745,9 +745,9 @@ export interface TextField {
|
||||
* via the `definition` "uploads".
|
||||
*/
|
||||
export interface Upload {
|
||||
id: string;
|
||||
id: number;
|
||||
text?: string | null;
|
||||
media?: (string | null) | Upload;
|
||||
media?: (number | null) | Upload;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
@@ -765,7 +765,7 @@ export interface Upload {
|
||||
* via the `definition` "array-fields".
|
||||
*/
|
||||
export interface ArrayField {
|
||||
id: string;
|
||||
id: number;
|
||||
title?: string | null;
|
||||
items: {
|
||||
text: string;
|
||||
@@ -863,7 +863,7 @@ export interface ArrayField {
|
||||
* via the `definition` "OnDemandForm".
|
||||
*/
|
||||
export interface OnDemandForm {
|
||||
id: string;
|
||||
id: number;
|
||||
json?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
@@ -881,7 +881,7 @@ export interface OnDemandForm {
|
||||
* via the `definition` "OnDemandOutsideForm".
|
||||
*/
|
||||
export interface OnDemandOutsideForm {
|
||||
id: string;
|
||||
id: number;
|
||||
json?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
@@ -891,6 +891,21 @@ export interface OnDemandOutsideForm {
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
hiddenAnchor?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: any;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
} | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -899,7 +914,7 @@ export interface OnDemandOutsideForm {
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
id: number;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
@@ -923,15 +938,15 @@ export interface User {
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
id: number;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'lexical-fully-featured';
|
||||
value: string | LexicalFullyFeatured;
|
||||
value: number | LexicalFullyFeatured;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'lexical-link-feature';
|
||||
value: string | LexicalLinkFeature;
|
||||
value: number | LexicalLinkFeature;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'lexical-heading-feature';
|
||||
@@ -939,68 +954,68 @@ export interface PayloadLockedDocument {
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'lexical-jsx-converter';
|
||||
value: string | LexicalJsxConverter;
|
||||
value: number | LexicalJsxConverter;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'lexical-fields';
|
||||
value: string | LexicalField;
|
||||
value: number | LexicalField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'lexical-migrate-fields';
|
||||
value: string | LexicalMigrateField;
|
||||
value: number | LexicalMigrateField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'lexical-localized-fields';
|
||||
value: string | LexicalLocalizedField;
|
||||
value: number | LexicalLocalizedField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'lexicalObjectReferenceBug';
|
||||
value: string | LexicalObjectReferenceBug;
|
||||
value: number | LexicalObjectReferenceBug;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'LexicalInBlock';
|
||||
value: string | LexicalInBlock;
|
||||
value: number | LexicalInBlock;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'lexical-access-control';
|
||||
value: string | LexicalAccessControl;
|
||||
value: number | LexicalAccessControl;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'lexical-relationship-fields';
|
||||
value: string | LexicalRelationshipField;
|
||||
value: number | LexicalRelationshipField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'rich-text-fields';
|
||||
value: string | RichTextField;
|
||||
value: number | RichTextField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'text-fields';
|
||||
value: string | TextField;
|
||||
value: number | TextField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'uploads';
|
||||
value: string | Upload;
|
||||
value: number | Upload;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'array-fields';
|
||||
value: string | ArrayField;
|
||||
value: number | ArrayField;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'OnDemandForm';
|
||||
value: string | OnDemandForm;
|
||||
value: number | OnDemandForm;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'OnDemandOutsideForm';
|
||||
value: string | OnDemandOutsideForm;
|
||||
value: number | OnDemandOutsideForm;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -1010,10 +1025,10 @@ export interface PayloadLockedDocument {
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
id: number;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
@@ -1033,7 +1048,7 @@ export interface PayloadPreference {
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
id: number;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
@@ -1388,6 +1403,7 @@ export interface OnDemandFormSelect<T extends boolean = true> {
|
||||
*/
|
||||
export interface OnDemandOutsideFormSelect<T extends boolean = true> {
|
||||
json?: T;
|
||||
hiddenAnchor?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
@@ -1450,7 +1466,7 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
* via the `definition` "tabsWithRichText".
|
||||
*/
|
||||
export interface TabsWithRichText {
|
||||
id: string;
|
||||
id: number;
|
||||
tab1?: {
|
||||
rt1?: {
|
||||
root: {
|
||||
@@ -1524,7 +1540,7 @@ export interface LexicalBlocksRadioButtonsBlock {
|
||||
export interface AvatarGroupBlock {
|
||||
avatars?:
|
||||
| {
|
||||
image?: (string | null) | Upload;
|
||||
image?: (number | null) | Upload;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
|
||||
Reference in New Issue
Block a user