feat: Custom Error, Label, and before/after field components (#3747)
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
This commit is contained in:
committed by
GitHub
parent
67b3baaa44
commit
266c3274d0
9
test/fields/collections/Text/AfterInput.tsx
Normal file
9
test/fields/collections/Text/AfterInput.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
const AfterInputImpl: React.FC = () => {
|
||||
return <label className="after-input">#after-input</label>
|
||||
}
|
||||
|
||||
export const AfterInput = <AfterInputImpl />
|
||||
9
test/fields/collections/Text/BeforeInput.tsx
Normal file
9
test/fields/collections/Text/BeforeInput.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
const BeforeInputImpl: React.FC = () => {
|
||||
return <label className="before-input">#before-input</label>
|
||||
}
|
||||
|
||||
export const BeforeInput = <BeforeInputImpl />
|
||||
15
test/fields/collections/Text/CustomError.tsx
Normal file
15
test/fields/collections/Text/CustomError.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { Props } from '../../../../packages/payload/src/admin/components/forms/Error/types'
|
||||
|
||||
const CustomError: React.FC<Props> = (props) => {
|
||||
const { showError = false } = props
|
||||
|
||||
if (showError) {
|
||||
return <div className="custom-error">#custom-error</div>
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default CustomError
|
||||
13
test/fields/collections/Text/CustomLabel.tsx
Normal file
13
test/fields/collections/Text/CustomLabel.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
const CustomLabel: React.FC<{ htmlFor: string }> = ({ htmlFor }) => {
|
||||
return (
|
||||
<label htmlFor={htmlFor} className="custom-label">
|
||||
#label
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomLabel
|
||||
@@ -1,51 +1,51 @@
|
||||
import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types'
|
||||
|
||||
import { textFieldsSlug } from '../../slugs'
|
||||
|
||||
export const defaultText = 'default-text'
|
||||
import { AfterInput } from './AfterInput'
|
||||
import { BeforeInput } from './BeforeInput'
|
||||
import CustomError from './CustomError'
|
||||
import CustomLabel from './CustomLabel'
|
||||
import { defaultText, textFieldsSlug } from './shared'
|
||||
|
||||
const TextFields: CollectionConfig = {
|
||||
slug: textFieldsSlug,
|
||||
admin: {
|
||||
useAsTitle: 'text',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
required: true,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'localizedText',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'i18nText',
|
||||
type: 'text',
|
||||
label: {
|
||||
en: 'Text en',
|
||||
es: 'Text es',
|
||||
},
|
||||
admin: {
|
||||
placeholder: {
|
||||
en: 'en placeholder',
|
||||
es: 'es placeholder',
|
||||
},
|
||||
description: {
|
||||
en: 'en description',
|
||||
es: 'es description',
|
||||
},
|
||||
placeholder: {
|
||||
en: 'en placeholder',
|
||||
es: 'es placeholder',
|
||||
},
|
||||
},
|
||||
label: {
|
||||
en: 'Text en',
|
||||
es: 'Text es',
|
||||
},
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'defaultFunction',
|
||||
type: 'text',
|
||||
defaultValue: () => defaultText,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'defaultAsync',
|
||||
type: 'text',
|
||||
defaultValue: async (): Promise<string> => {
|
||||
return new Promise((resolve) =>
|
||||
setTimeout(() => {
|
||||
@@ -53,25 +53,25 @@ const TextFields: CollectionConfig = {
|
||||
}, 1),
|
||||
)
|
||||
},
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Override the 40k text length default',
|
||||
name: 'overrideLength',
|
||||
type: 'text',
|
||||
label: 'Override the 40k text length default',
|
||||
maxLength: 50000,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'fieldWithDefaultValue',
|
||||
type: 'text',
|
||||
defaultValue: async () => {
|
||||
const defaultValue = new Promise((resolve) => setTimeout(() => resolve('some-value'), 1000))
|
||||
|
||||
return defaultValue
|
||||
},
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'dependentOnFieldWithDefaultValue',
|
||||
type: 'text',
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
({ data }) => {
|
||||
@@ -79,13 +79,39 @@ const TextFields: CollectionConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'customLabel',
|
||||
admin: {
|
||||
components: {
|
||||
Label: CustomLabel,
|
||||
},
|
||||
},
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'customError',
|
||||
admin: {
|
||||
components: {
|
||||
Error: CustomError,
|
||||
},
|
||||
},
|
||||
minLength: 3,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'beforeAndAfterInput',
|
||||
admin: {
|
||||
components: {
|
||||
AfterInput: [AfterInput],
|
||||
BeforeInput: [BeforeInput],
|
||||
},
|
||||
},
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const textDoc = {
|
||||
text: 'Seeded text document',
|
||||
localizedText: 'Localized text',
|
||||
slug: textFieldsSlug,
|
||||
}
|
||||
|
||||
export default TextFields
|
||||
|
||||
6
test/fields/collections/Text/shared.ts
Normal file
6
test/fields/collections/Text/shared.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const defaultText = 'default-text'
|
||||
export const textFieldsSlug = 'text-fields'
|
||||
export const textDoc = {
|
||||
text: 'Seeded text document',
|
||||
localizedText: 'Localized text',
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import { initPayloadE2E } from '../helpers/configHelpers'
|
||||
import { RESTClient } from '../helpers/rest'
|
||||
import { jsonDoc } from './collections/JSON'
|
||||
import { numberDoc } from './collections/Number'
|
||||
import { textDoc } from './collections/Text'
|
||||
import { textDoc } from './collections/Text/shared'
|
||||
import { lexicalE2E } from './lexicalE2E'
|
||||
import { clearAndSeedEverything } from './seed'
|
||||
import {
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
textFieldsSlug,
|
||||
} from './slugs'
|
||||
|
||||
const { afterEach, beforeAll, describe, beforeEach } = test
|
||||
const { afterEach, beforeAll, beforeEach, describe } = test
|
||||
|
||||
let client: RESTClient
|
||||
let page: Page
|
||||
@@ -34,7 +34,7 @@ describe('fields', () => {
|
||||
beforeAll(async ({ browser }) => {
|
||||
const config = await initPayloadE2E(__dirname)
|
||||
serverURL = config.serverURL
|
||||
client = new RESTClient(null, { serverURL, defaultSlug: 'users' })
|
||||
client = new RESTClient(null, { defaultSlug: 'users', serverURL })
|
||||
await client.login()
|
||||
|
||||
const context = await browser.newContext()
|
||||
@@ -43,13 +43,13 @@ describe('fields', () => {
|
||||
beforeEach(async () => {
|
||||
await clearAndSeedEverything(payload)
|
||||
await client.logout()
|
||||
client = new RESTClient(null, { serverURL, defaultSlug: 'users' })
|
||||
client = new RESTClient(null, { defaultSlug: 'users', serverURL })
|
||||
await client.login()
|
||||
})
|
||||
describe('text', () => {
|
||||
let url: AdminUrlUtil
|
||||
beforeAll(() => {
|
||||
url = new AdminUrlUtil(serverURL, 'text-fields')
|
||||
url = new AdminUrlUtil(serverURL, textFieldsSlug)
|
||||
})
|
||||
|
||||
test('should display field in list view', async () => {
|
||||
@@ -80,6 +80,40 @@ describe('fields', () => {
|
||||
const description = page.locator('.field-description-i18nText')
|
||||
await expect(description).toHaveText('en description')
|
||||
})
|
||||
|
||||
test('should render custom label', async () => {
|
||||
await page.goto(url.create)
|
||||
const label = page.locator('label.custom-label[for="field-customLabel"]')
|
||||
await expect(label).toHaveText('#label')
|
||||
})
|
||||
|
||||
test('should render custom error', async () => {
|
||||
await page.goto(url.create)
|
||||
const input = page.locator('input[id="field-customError"]')
|
||||
await input.fill('ab')
|
||||
await expect(input).toHaveValue('ab')
|
||||
const error = page.locator('.custom-error:near(input[id="field-customError"])')
|
||||
const submit = page.locator('button[type="button"][id="action-save"]')
|
||||
await submit.click()
|
||||
await expect(error).toHaveText('#custom-error')
|
||||
})
|
||||
|
||||
test('should render BeforeInput and AfterInput', async () => {
|
||||
await page.goto(url.create)
|
||||
const input = page.locator('input[id="field-beforeAndAfterInput"]')
|
||||
|
||||
const prevSibling = await input.evaluateHandle((el) => {
|
||||
return el.previousElementSibling
|
||||
})
|
||||
const prevSiblingText = await page.evaluate((el) => el.textContent, prevSibling)
|
||||
await expect(prevSiblingText).toEqual('#before-input')
|
||||
|
||||
const nextSibling = await input.evaluateHandle((el) => {
|
||||
return el.nextElementSibling
|
||||
})
|
||||
const nextSiblingText = await page.evaluate((el) => el.textContent, nextSibling)
|
||||
await expect(nextSiblingText).toEqual('#after-input')
|
||||
})
|
||||
})
|
||||
|
||||
describe('number', () => {
|
||||
@@ -166,11 +200,11 @@ describe('fields', () => {
|
||||
await payload.create({
|
||||
collection: 'indexed-fields',
|
||||
data: {
|
||||
text: 'text',
|
||||
uniqueText,
|
||||
group: {
|
||||
unique: uniqueText,
|
||||
},
|
||||
text: 'text',
|
||||
uniqueText,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -286,17 +320,17 @@ describe('fields', () => {
|
||||
filledGroupPoint = await payload.create({
|
||||
collection: pointFieldsSlug,
|
||||
data: {
|
||||
point: [5, 5],
|
||||
localized: [4, 2],
|
||||
group: { point: [4, 2] },
|
||||
localized: [4, 2],
|
||||
point: [5, 5],
|
||||
},
|
||||
})
|
||||
emptyGroupPoint = await payload.create({
|
||||
collection: pointFieldsSlug,
|
||||
data: {
|
||||
point: [5, 5],
|
||||
localized: [3, -2],
|
||||
group: {},
|
||||
localized: [3, -2],
|
||||
point: [5, 5],
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -1139,8 +1173,8 @@ describe('fields', () => {
|
||||
describe('EST', () => {
|
||||
test.use({
|
||||
geolocation: {
|
||||
longitude: -83.0458,
|
||||
latitude: 42.3314,
|
||||
longitude: -83.0458,
|
||||
},
|
||||
timezoneId: 'America/Detroit',
|
||||
})
|
||||
@@ -1160,7 +1194,7 @@ describe('fields', () => {
|
||||
const id = routeSegments.pop()
|
||||
|
||||
// fetch the doc (need the date string from the DB)
|
||||
const { doc } = await client.findByID({ id, slug: 'date-fields', auth: true })
|
||||
const { doc } = await client.findByID({ id, auth: true, slug: 'date-fields' })
|
||||
|
||||
expect(doc.default).toEqual('2023-02-07T12:00:00.000Z')
|
||||
})
|
||||
@@ -1169,8 +1203,8 @@ describe('fields', () => {
|
||||
describe('PST', () => {
|
||||
test.use({
|
||||
geolocation: {
|
||||
longitude: -122.419416,
|
||||
latitude: 37.774929,
|
||||
longitude: -122.419416,
|
||||
},
|
||||
timezoneId: 'America/Los_Angeles',
|
||||
})
|
||||
@@ -1191,7 +1225,7 @@ describe('fields', () => {
|
||||
const id = routeSegments.pop()
|
||||
|
||||
// fetch the doc (need the date string from the DB)
|
||||
const { doc } = await client.findByID({ id, slug: 'date-fields', auth: true })
|
||||
const { doc } = await client.findByID({ id, auth: true, slug: 'date-fields' })
|
||||
|
||||
expect(doc.default).toEqual('2023-02-07T12:00:00.000Z')
|
||||
})
|
||||
@@ -1200,8 +1234,8 @@ describe('fields', () => {
|
||||
describe('ST', () => {
|
||||
test.use({
|
||||
geolocation: {
|
||||
longitude: -171.857,
|
||||
latitude: -14.5994,
|
||||
longitude: -171.857,
|
||||
},
|
||||
timezoneId: 'Pacific/Apia',
|
||||
})
|
||||
@@ -1222,7 +1256,7 @@ describe('fields', () => {
|
||||
const id = routeSegments.pop()
|
||||
|
||||
// fetch the doc (need the date string from the DB)
|
||||
const { doc } = await client.findByID({ id, slug: 'date-fields', auth: true })
|
||||
const { doc } = await client.findByID({ id, auth: true, slug: 'date-fields' })
|
||||
|
||||
expect(doc.default).toEqual('2023-02-07T12:00:00.000Z')
|
||||
})
|
||||
@@ -1245,7 +1279,7 @@ describe('fields', () => {
|
||||
})
|
||||
const relationshipIDs = allRelationshipDocs.docs.map((doc) => doc.id)
|
||||
await mapAsync(relationshipIDs, async (id) => {
|
||||
await payload.delete({ collection: relationshipFieldsSlug, id })
|
||||
await payload.delete({ id, collection: relationshipFieldsSlug })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
namedTabDefaultValue,
|
||||
namedTabText,
|
||||
} from './collections/Tabs/constants'
|
||||
import { defaultText } from './collections/Text'
|
||||
import { defaultText } from './collections/Text/shared'
|
||||
import { clearAndSeedEverything } from './seed'
|
||||
import { arrayFieldsSlug, groupFieldsSlug, relationshipFieldsSlug, tabsFieldsSlug } from './slugs'
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import { radiosDoc } from './collections/Radio'
|
||||
import { richTextBulletsDoc, richTextDoc } from './collections/RichText/data'
|
||||
import { selectsDoc } from './collections/Select'
|
||||
import { tabsDoc } from './collections/Tabs'
|
||||
import { textDoc } from './collections/Text'
|
||||
import { textDoc } from './collections/Text/shared'
|
||||
import { uploadsDoc } from './collections/Upload'
|
||||
import {
|
||||
blockFieldsSlug,
|
||||
@@ -45,11 +45,8 @@ import {
|
||||
|
||||
export async function clearAndSeedEverything(_payload: Payload) {
|
||||
return await seedDB({
|
||||
snapshotKey: 'fieldsTest',
|
||||
shouldResetDB: true,
|
||||
collectionSlugs,
|
||||
_payload,
|
||||
uploadsDir: path.resolve(__dirname, './collections/Upload/uploads'),
|
||||
collectionSlugs,
|
||||
seedFunction: async (_payload) => {
|
||||
const jpgPath = path.resolve(__dirname, './collections/Upload/payload.jpg')
|
||||
const pngPath = path.resolve(__dirname, './uploads/payload.png')
|
||||
@@ -143,5 +140,8 @@ export async function clearAndSeedEverything(_payload: Payload) {
|
||||
_payload.create({ collection: numberFieldsSlug, data: numberDoc }),
|
||||
])
|
||||
},
|
||||
shouldResetDB: true,
|
||||
snapshotKey: 'fieldsTest',
|
||||
uploadsDir: path.resolve(__dirname, './collections/Upload/uploads'),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user