feat: Custom Error, Label, and before/after field components (#3747)

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
This commit is contained in:
Hulpoi George-Valentin
2023-11-08 14:40:31 -05:00
committed by GitHub
parent 67b3baaa44
commit 266c3274d0
32 changed files with 729 additions and 167 deletions

View 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 />

View 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 />

View 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

View 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

View File

@@ -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

View 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',
}

View File

@@ -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 })
})
})

View File

@@ -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'

View File

@@ -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'),
})
}