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. |
|
| **`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. |
|
| **`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. |
|
| **`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. |
|
| **`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. |
|
| **`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. |
|
| **`disableListFilter`** | Set `disableListFilter` to `true` to prevent fields from appearing in the list view filter options. |
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { DiffMethod } from 'react-diff-viewer-continued'
|
import type { DiffMethod } from 'react-diff-viewer-continued'
|
||||||
|
|
||||||
import { fieldAffectsData } from 'payload/shared'
|
import { fieldAffectsData, fieldIsID } from 'payload/shared'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import type { diffComponents as _diffComponents } from './fields/index.js'
|
import type { diffComponents as _diffComponents } from './fields/index.js'
|
||||||
@@ -29,7 +29,7 @@ const RenderFieldsToDiff: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<div className={baseClass}>
|
<div className={baseClass}>
|
||||||
{fields?.map((field, i) => {
|
{fields?.map((field, i) => {
|
||||||
if ('name' in field && field.name === 'id') {
|
if (fieldIsID(field)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -234,7 +234,6 @@ export const createClientCollectionConfig = ({
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
break
|
|
||||||
default:
|
default:
|
||||||
clientCollection[key] = collection[key]
|
clientCollection[key] = collection[key]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export {
|
|||||||
fieldIsArrayType,
|
fieldIsArrayType,
|
||||||
fieldIsBlockType,
|
fieldIsBlockType,
|
||||||
fieldIsGroupType,
|
fieldIsGroupType,
|
||||||
|
fieldIsHiddenOrDisabled,
|
||||||
|
fieldIsID,
|
||||||
fieldIsLocalized,
|
fieldIsLocalized,
|
||||||
fieldIsPresentationalOnly,
|
fieldIsPresentationalOnly,
|
||||||
fieldIsSidebar,
|
fieldIsSidebar,
|
||||||
|
|||||||
@@ -75,28 +75,24 @@ export const createClientField = ({
|
|||||||
}): ClientField => {
|
}): ClientField => {
|
||||||
const clientField: ClientField = {} as 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) {
|
for (const key in incomingField) {
|
||||||
if (serverOnlyFieldProperties.includes(key as any)) {
|
if (serverOnlyFieldProperties.includes(key as any)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'admin':
|
case 'admin':
|
||||||
if (!incomingField.admin) {
|
if (!incomingField.admin) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
clientField.admin = {} as AdminClient
|
clientField.admin = {} as AdminClient
|
||||||
|
|
||||||
for (const adminKey in incomingField.admin) {
|
for (const adminKey in incomingField.admin) {
|
||||||
if (serverOnlyFieldAdminProperties.includes(adminKey as any)) {
|
if (serverOnlyFieldAdminProperties.includes(adminKey as any)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (adminKey) {
|
switch (adminKey) {
|
||||||
case 'description':
|
case 'description':
|
||||||
if ('description' in incomingField.admin) {
|
if ('description' in incomingField.admin) {
|
||||||
@@ -107,16 +103,20 @@ export const createClientField = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
clientField.admin[adminKey] = incomingField.admin[adminKey]
|
clientField.admin[adminKey] = incomingField.admin[adminKey]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'blocks':
|
case 'blocks':
|
||||||
case 'fields':
|
case 'fields':
|
||||||
case 'tabs':
|
case 'tabs':
|
||||||
// Skip - we handle sub-fields in the switch below
|
// Skip - we handle sub-fields in the switch below
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'label':
|
case 'label':
|
||||||
//@ts-expect-error - would need to type narrow
|
//@ts-expect-error - would need to type narrow
|
||||||
if (typeof incomingField.label === 'function') {
|
if (typeof incomingField.label === 'function') {
|
||||||
@@ -126,7 +126,9 @@ export const createClientField = ({
|
|||||||
//@ts-expect-error - would need to type narrow
|
//@ts-expect-error - would need to type narrow
|
||||||
clientField.label = incomingField.label
|
clientField.label = incomingField.label
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
clientField[key] = incomingField[key]
|
clientField[key] = incomingField[key]
|
||||||
}
|
}
|
||||||
@@ -243,6 +245,7 @@ export const createClientField = ({
|
|||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'richText': {
|
case 'richText': {
|
||||||
if (!incomingField?.editor) {
|
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
|
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)) {
|
if (serverOnlyFieldProperties.includes(key as any)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === 'fields') {
|
if (key === 'fields') {
|
||||||
clientTab.fields = createClientFields({
|
clientTab.fields = createClientFields({
|
||||||
defaultIDType,
|
defaultIDType,
|
||||||
@@ -320,10 +324,8 @@ export const createClientFields = ({
|
|||||||
importMap,
|
importMap,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (clientField) {
|
|
||||||
clientFields.push(clientField)
|
clientFields.push(clientField)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const hasID = flattenTopLevelFields(fields).some((f) => fieldAffectsData(f) && f.name === 'id')
|
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'
|
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<
|
export function fieldAffectsData<
|
||||||
TField extends ClientField | Field | TabAsField | TabAsFieldClient,
|
TField extends ClientField | Field | TabAsField | TabAsFieldClient,
|
||||||
>(
|
>(
|
||||||
|
|||||||
@@ -590,6 +590,7 @@ export class BasePayload {
|
|||||||
if (!fieldAffectsData(field)) {
|
if (!fieldAffectsData(field)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.name === 'id') {
|
if (field.name === 'id') {
|
||||||
customIDType = field.type
|
customIDType = field.type
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -52,11 +52,11 @@ export const fieldSchemaToJSON = (fields: ClientField[]): FieldSchemaJSON => {
|
|||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'collapsible':
|
case 'collapsible': // eslint-disable no-fallthrough
|
||||||
|
|
||||||
case 'row':
|
case 'row':
|
||||||
result = result.concat(fieldSchemaToJSON(field.fields))
|
result = result.concat(fieldSchemaToJSON(field.fields))
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'group':
|
case 'group':
|
||||||
acc.push({
|
acc.push({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
@@ -66,8 +66,7 @@ export const fieldSchemaToJSON = (fields: ClientField[]): FieldSchemaJSON => {
|
|||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'relationship':
|
case 'relationship': // eslint-disable no-fallthrough
|
||||||
|
|
||||||
case 'upload':
|
case 'upload':
|
||||||
acc.push({
|
acc.push({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
@@ -77,6 +76,7 @@ export const fieldSchemaToJSON = (fields: ClientField[]): FieldSchemaJSON => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'tabs': {
|
case 'tabs': {
|
||||||
let tabFields = []
|
let tabFields = []
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { ClientField, FieldWithPath, FormState } from 'payload'
|
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 React, { Fragment, useState } from 'react'
|
||||||
|
|
||||||
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js'
|
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js'
|
||||||
@@ -69,7 +69,7 @@ const reduceFields = ({
|
|||||||
(fieldAffectsData(field) || field.type === 'ui') &&
|
(fieldAffectsData(field) || field.type === 'ui') &&
|
||||||
(field.admin.disableBulkEdit ||
|
(field.admin.disableBulkEdit ||
|
||||||
field.unique ||
|
field.unique ||
|
||||||
field.admin.hidden ||
|
fieldIsHiddenOrDisabled(field) ||
|
||||||
('readOnly' in field && field.readOnly))
|
('readOnly' in field && field.readOnly))
|
||||||
) {
|
) {
|
||||||
return fieldsToUse
|
return fieldsToUse
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { DefaultCellComponentProps, UploadFieldClient } from 'payload'
|
|||||||
|
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import LinkImport from 'next/link.js'
|
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 React from 'react' // TODO: abstract this out to support all routers
|
||||||
|
|
||||||
import { useConfig } from '../../../providers/Config/index.js'
|
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 (
|
return (
|
||||||
<WrapElement {...wrapElementProps}>
|
<WrapElement {...wrapElementProps}>
|
||||||
<CodeCell
|
<CodeCell
|
||||||
|
|||||||
@@ -11,7 +11,12 @@ import type {
|
|||||||
} from 'payload'
|
} from 'payload'
|
||||||
|
|
||||||
import { MissingEditorProp } 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 React from 'react'
|
||||||
|
|
||||||
import type { ColumnPreferences } from '../../providers/ListQuery/index.js'
|
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
|
// place the `ID` field first, if it exists
|
||||||
// do the same for the `useAsTitle` field with precedence over the `ID` field
|
// 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`
|
// 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) {
|
if (idFieldIndex > -1) {
|
||||||
const idField = sortedFieldMap.splice(idFieldIndex, 1)[0]
|
const idField = sortedFieldMap.splice(idFieldIndex, 1)[0]
|
||||||
@@ -117,6 +122,10 @@ export const buildColumnState = (args: Args): Column[] => {
|
|||||||
const activeColumnsIndices = []
|
const activeColumnsIndices = []
|
||||||
|
|
||||||
const sorted: Column[] = sortedFieldMap?.reduce((acc, field, index) => {
|
const sorted: Column[] = sortedFieldMap?.reduce((acc, field, index) => {
|
||||||
|
if (fieldIsHiddenOrDisabled(field) && !fieldIsID(field)) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
const _field = _sortedFieldMap.find(
|
const _field = _sortedFieldMap.find(
|
||||||
(f) => 'name' in field && 'name' in f && f.name === field.name,
|
(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 type { ClientField } from 'payload'
|
||||||
|
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import { tabHasName } from 'payload/shared'
|
import { fieldIsHiddenOrDisabled, fieldIsID, tabHasName } from 'payload/shared'
|
||||||
|
|
||||||
import type { FieldCondition } from './types.js'
|
import type { FieldCondition } from './types.js'
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ export const reduceClientFields = ({
|
|||||||
pathPrefix,
|
pathPrefix,
|
||||||
}: ReduceClientFieldsArgs): FieldCondition[] => {
|
}: ReduceClientFieldsArgs): FieldCondition[] => {
|
||||||
return fields.reduce((reduced, field) => {
|
return fields.reduce((reduced, field) => {
|
||||||
if (field.admin?.disableListFilter) {
|
if (field.admin?.disableListFilter || (fieldIsHiddenOrDisabled(field) && !fieldIsID(field))) {
|
||||||
return reduced
|
return reduced
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +163,6 @@ export const reduceClientFields = ({
|
|||||||
reduced.push(formattedField)
|
reduced.push(formattedField)
|
||||||
return reduced
|
return reduced
|
||||||
}
|
}
|
||||||
|
|
||||||
return reduced
|
return reduced
|
||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { getFieldPaths } from 'payload/shared'
|
import { fieldIsHiddenOrDisabled, getFieldPaths } from 'payload/shared'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import type { RenderFieldsProps } from './types.js'
|
import type { RenderFieldsProps } from './types.js'
|
||||||
@@ -45,7 +45,7 @@ export const RenderFields: React.FC<RenderFieldsProps> = (props) => {
|
|||||||
{fields.map((field, i) => {
|
{fields.map((field, i) => {
|
||||||
// For sidebar fields in the main fields array, `field` will be `null`, and visa versa
|
// 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)
|
// 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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
deepCopyObjectSimple,
|
deepCopyObjectSimple,
|
||||||
fieldAffectsData,
|
fieldAffectsData,
|
||||||
fieldHasSubFields,
|
fieldHasSubFields,
|
||||||
|
fieldIsHiddenOrDisabled,
|
||||||
|
fieldIsID,
|
||||||
fieldIsSidebar,
|
fieldIsSidebar,
|
||||||
getFieldPaths,
|
getFieldPaths,
|
||||||
tabHasName,
|
tabHasName,
|
||||||
@@ -138,11 +140,9 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
|||||||
|
|
||||||
const requiresRender = renderAllFields || previousFormState?.[path]?.requiresRender
|
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
|
let fieldPermissions: SanitizedFieldPermissions = true
|
||||||
if (fieldAffectsData(field) && !(isHiddenField || disabledFromAdmin)) {
|
|
||||||
|
if (fieldAffectsData(field) && !fieldIsHiddenOrDisabled(field)) {
|
||||||
fieldPermissions =
|
fieldPermissions =
|
||||||
parentPermissions === true
|
parentPermissions === true
|
||||||
? parentPermissions
|
? parentPermissions
|
||||||
@@ -233,7 +233,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
|||||||
if (!omitParents && (!filter || filter(args))) {
|
if (!omitParents && (!filter || filter(args))) {
|
||||||
state[parentPath + '.id'] = {
|
state[parentPath + '.id'] = {
|
||||||
fieldSchema: includeSchema
|
fieldSchema: includeSchema
|
||||||
? field.fields.find((field) => 'name' in field && field.name === 'id')
|
? field.fields.find((field) => fieldIsID(field))
|
||||||
: undefined,
|
: undefined,
|
||||||
initialValue: row.id,
|
initialValue: row.id,
|
||||||
valid: true,
|
valid: true,
|
||||||
@@ -343,9 +343,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
|
|||||||
if (!omitParents && (!filter || filter(args))) {
|
if (!omitParents && (!filter || filter(args))) {
|
||||||
state[parentPath + '.id'] = {
|
state[parentPath + '.id'] = {
|
||||||
fieldSchema: includeSchema
|
fieldSchema: includeSchema
|
||||||
? block.fields.find(
|
? block.fields.find((blockField) => fieldIsID(blockField))
|
||||||
(blockField) => 'name' in blockField && blockField.name === 'id',
|
|
||||||
)
|
|
||||||
: undefined,
|
: undefined,
|
||||||
initialValue: row.id,
|
initialValue: row.id,
|
||||||
valid: true,
|
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 && renderFieldFn && !fieldIsHiddenOrDisabled(field)) {
|
||||||
|
|
||||||
if (requiresRender && !isDisabled && renderFieldFn) {
|
|
||||||
const fieldState = state[path]
|
const fieldState = state[path]
|
||||||
|
|
||||||
const fieldConfig = fieldSchemaMap.get(schemaPath)
|
const fieldConfig = fieldSchemaMap.get(schemaPath)
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import type { ClientComponentProps, ClientField, FieldPaths, ServerComponentProp
|
|||||||
|
|
||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import { createClientField, MissingEditorProp } from 'payload'
|
import { createClientField, MissingEditorProp } from 'payload'
|
||||||
|
import { fieldIsHiddenOrDisabled } from 'payload/shared'
|
||||||
|
|
||||||
import type { RenderFieldMethod } from './types.js'
|
import type { RenderFieldMethod } from './types.js'
|
||||||
|
|
||||||
import { RenderServerComponent } from '../../elements/RenderServerComponent/index.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
|
// 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'
|
import { FieldDescription } from '../../exports/client/index.js'
|
||||||
|
|
||||||
@@ -45,6 +47,10 @@ export const renderField: RenderFieldMethod = ({
|
|||||||
importMap: req.payload.importMap,
|
importMap: req.payload.importMap,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (fieldIsHiddenOrDisabled(clientField)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const clientProps: ClientComponentProps & Partial<FieldPaths> = {
|
const clientProps: ClientComponentProps & Partial<FieldPaths> = {
|
||||||
customComponents: fieldState?.customComponents || {},
|
customComponents: fieldState?.customComponents || {},
|
||||||
field: clientField,
|
field: clientField,
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export const buildTableState = async (
|
|||||||
if (!canAccessAdmin) {
|
if (!canAccessAdmin) {
|
||||||
throw new Error('Unauthorized')
|
throw new Error('Unauthorized')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match the user collection to the global admin config
|
// Match the user collection to the global admin config
|
||||||
} else if (adminUserSlug !== incomingUserSlug) {
|
} else if (adminUserSlug !== incomingUserSlug) {
|
||||||
throw new Error('Unauthorized')
|
throw new Error('Unauthorized')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Field } from 'payload'
|
import type { Field } from 'payload'
|
||||||
|
|
||||||
import { fieldAffectsData } from 'payload/shared'
|
import { fieldAffectsData, fieldIsID } from 'payload/shared'
|
||||||
|
|
||||||
export const formatFields = (fields: Field[], isEditing?: boolean): Field[] =>
|
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'
|
} from 'payload'
|
||||||
|
|
||||||
import { getTranslation, type I18nClient } from '@payloadcms/translations'
|
import { getTranslation, type I18nClient } from '@payloadcms/translations'
|
||||||
|
import { fieldIsHiddenOrDisabled, fieldIsID } from 'payload/shared'
|
||||||
|
|
||||||
// eslint-disable-next-line payload/no-imports-from-exports-dir
|
// eslint-disable-next-line payload/no-imports-from-exports-dir
|
||||||
import type { Column } from '../exports/client/index.js'
|
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 { buildColumnState } from '../elements/TableColumns/buildColumnState.js'
|
||||||
import { filterFields } from '../elements/TableColumns/filterFields.js'
|
import { filterFields } from '../elements/TableColumns/filterFields.js'
|
||||||
import { getInitialColumns } from '../elements/TableColumns/getInitialColumns.js'
|
import { getInitialColumns } from '../elements/TableColumns/getInitialColumns.js'
|
||||||
|
|
||||||
// eslint-disable-next-line payload/no-imports-from-exports-dir
|
// eslint-disable-next-line payload/no-imports-from-exports-dir
|
||||||
import { Pill, Table } from '../exports/client/index.js'
|
import { Pill, Table } from '../exports/client/index.js'
|
||||||
|
|
||||||
@@ -26,6 +28,10 @@ export const renderFilters = (
|
|||||||
): Map<string, React.ReactNode> =>
|
): Map<string, React.ReactNode> =>
|
||||||
fields.reduce(
|
fields.reduce(
|
||||||
(acc, field) => {
|
(acc, field) => {
|
||||||
|
if (fieldIsHiddenOrDisabled(field)) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
if ('name' in field && field.admin?.components?.Filter) {
|
if ('name' in field && field.admin?.components?.Filter) {
|
||||||
acc.set(
|
acc.set(
|
||||||
field.name,
|
field.name,
|
||||||
|
|||||||
@@ -218,6 +218,27 @@ export interface CustomField {
|
|||||||
descriptionAsFunction?: string | null;
|
descriptionAsFunction?: string | null;
|
||||||
descriptionAsComponent?: string | null;
|
descriptionAsComponent?: string | null;
|
||||||
customSelectField?: 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;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -538,6 +559,32 @@ export interface CustomFieldsSelect<T extends boolean = true> {
|
|||||||
descriptionAsFunction?: T;
|
descriptionAsFunction?: T;
|
||||||
descriptionAsComponent?: T;
|
descriptionAsComponent?: T;
|
||||||
customSelectField?: 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;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Page } from '@playwright/test'
|
import type { Page } from '@playwright/test'
|
||||||
|
|
||||||
import { expect, test } from '@playwright/test'
|
import { expect, test } from '@playwright/test'
|
||||||
|
import { openListColumns, toggleColumn } from 'helpers/e2e/toggleColumn.js'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { wait } from 'payload/shared'
|
import { wait } from 'payload/shared'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
exactText,
|
exactText,
|
||||||
initPageConsoleErrorCatch,
|
initPageConsoleErrorCatch,
|
||||||
saveDocAndAssert,
|
saveDocAndAssert,
|
||||||
|
selectTableRow,
|
||||||
} from '../../../helpers.js'
|
} from '../../../helpers.js'
|
||||||
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
||||||
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
|
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
|
||||||
@@ -67,6 +69,93 @@ describe('Text', () => {
|
|||||||
await ensureCompilationIsDone({ page, serverURL })
|
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 () => {
|
test('should display field in list view', async () => {
|
||||||
await page.goto(url.list)
|
await page.goto(url.list)
|
||||||
const textCell = page.locator('.row-1 .cell-text')
|
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 () => {
|
test('should display i18n label in cells when missing field data', async () => {
|
||||||
await page.goto(url.list)
|
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')
|
const textCell = page.locator('.row-1 .cell-i18nText')
|
||||||
|
|
||||||
await expect(textCell).toHaveText('<No Text en>')
|
await expect(textCell).toHaveText('<No Text en>')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,36 @@ const TextFields: CollectionConfig = {
|
|||||||
beforeDuplicate: [({ value }) => `${value} - duplicate`],
|
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',
|
name: 'localizedText',
|
||||||
type: 'text',
|
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 })
|
await ensureCompilationIsDone({ page, serverURL })
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await reInitializeDB({
|
await reInitializeDB({
|
||||||
serverURL,
|
serverURL,
|
||||||
|
|||||||
@@ -869,6 +869,9 @@ export interface BlockField {
|
|||||||
export interface TextField {
|
export interface TextField {
|
||||||
id: string;
|
id: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
hiddenTextField?: string | null;
|
||||||
|
adminHiddenTextField?: string | null;
|
||||||
|
disabledTextField?: string | null;
|
||||||
localizedText?: string | null;
|
localizedText?: string | null;
|
||||||
i18nText?: string | null;
|
i18nText?: string | null;
|
||||||
defaultString?: string | null;
|
defaultString?: string | null;
|
||||||
@@ -3234,6 +3237,9 @@ export interface TabsFieldsSelect<T extends boolean = true> {
|
|||||||
*/
|
*/
|
||||||
export interface TextFieldsSelect<T extends boolean = true> {
|
export interface TextFieldsSelect<T extends boolean = true> {
|
||||||
text?: T;
|
text?: T;
|
||||||
|
hiddenTextField?: T;
|
||||||
|
adminHiddenTextField?: T;
|
||||||
|
disabledTextField?: T;
|
||||||
localizedText?: T;
|
localizedText?: T;
|
||||||
i18nText?: T;
|
i18nText?: T;
|
||||||
defaultString?: T;
|
defaultString?: T;
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
import type { Page } from '@playwright/test'
|
import type { Page } from '@playwright/test'
|
||||||
|
|
||||||
import { expect } from '@playwright/test'
|
import { expect } from '@playwright/test'
|
||||||
|
import { wait } from 'payload/shared'
|
||||||
|
|
||||||
import { exactText } from '../../helpers.js'
|
import { exactText } from '../../helpers.js'
|
||||||
|
|
||||||
export const toggleColumn = async (
|
export const openListColumns = async (
|
||||||
page: Page,
|
page: Page,
|
||||||
{
|
{
|
||||||
togglerSelector = '.list-controls__toggle-columns',
|
togglerSelector = '.list-controls__toggle-columns',
|
||||||
columnContainerSelector = '.list-controls__columns',
|
columnContainerSelector = '.list-controls__columns',
|
||||||
columnLabel,
|
|
||||||
}: {
|
}: {
|
||||||
columnContainerSelector?: string
|
columnContainerSelector?: string
|
||||||
columnLabel: string
|
|
||||||
togglerSelector?: string
|
togglerSelector?: string
|
||||||
},
|
},
|
||||||
): Promise<any> => {
|
): Promise<any> => {
|
||||||
@@ -25,6 +24,25 @@ export const toggleColumn = async (
|
|||||||
|
|
||||||
await expect(page.locator(`${columnContainerSelector}.rah-static--height-auto`)).toBeVisible()
|
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`, {
|
const column = columnContainer.locator(`.column-selector .column-selector__column`, {
|
||||||
hasText: exactText(columnLabel),
|
hasText: exactText(columnLabel),
|
||||||
})
|
})
|
||||||
@@ -33,16 +51,24 @@ export const toggleColumn = async (
|
|||||||
el.classList.contains('column-selector__column--active'),
|
el.classList.contains('column-selector__column--active'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const targetState =
|
||||||
|
targetStateFromArgs !== undefined ? targetStateFromArgs : isActiveBeforeClick ? 'off' : 'on'
|
||||||
|
|
||||||
await expect(column).toBeVisible()
|
await expect(column).toBeVisible()
|
||||||
|
|
||||||
|
if (
|
||||||
|
(isActiveBeforeClick && targetState === 'off') ||
|
||||||
|
(!isActiveBeforeClick && targetState === 'on')
|
||||||
|
) {
|
||||||
await column.click()
|
await column.click()
|
||||||
|
}
|
||||||
|
|
||||||
if (isActiveBeforeClick) {
|
if (targetState === 'off') {
|
||||||
// no class
|
// no class
|
||||||
await expect(column).not.toHaveClass('column-selector__column--active')
|
await expect(column).not.toHaveClass(/column-selector__column--active/)
|
||||||
} else {
|
} else {
|
||||||
// has class
|
// has class
|
||||||
await expect(column).toHaveClass('column-selector__column--active')
|
await expect(column).toHaveClass(/column-selector__column--active/)
|
||||||
}
|
}
|
||||||
|
|
||||||
return column
|
return column
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@payload-config": [
|
"@payload-config": [
|
||||||
"./test/live-preview/config.ts"
|
"./test/_community/config.ts"
|
||||||
],
|
],
|
||||||
"@payloadcms/live-preview": [
|
"@payloadcms/live-preview": [
|
||||||
"./packages/live-preview/src"
|
"./packages/live-preview/src"
|
||||||
|
|||||||
Reference in New Issue
Block a user