When only a partial `labels` config is defined on a collection, the
collection defaults do not apply as expected. This leads to undefined
`singular` or `plural` properties that render as either empty or
untranslated strings on the front-end.
For example:
```ts
import type { CollectionConfig } from 'payload'
export MyCollection: CollectionConfig = {
// ...
labels: {
plural: 'Pages', // Notice that `singular` is excluded here
},
}
```
This renders empty or untranslated strings throughout the admin panel,
here are a couple examples:
<img width="326" height="211" alt="Screenshot 2025-09-26 at 10 27 40 AM"
src="https://github.com/user-attachments/assets/3872c4dd-0dac-4c1c-b417-61ddd042bbb8"
/>
<img width="330" height="267" alt="Screenshot 2025-09-26 at 10 27 51 AM"
src="https://github.com/user-attachments/assets/78772405-b5f3-45fa-9bf0-bc078f1ba976"
/>
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
- https://app.asana.com/0/0/1211478736160147
193 lines
5.6 KiB
TypeScript
193 lines
5.6 KiB
TypeScript
import { execSync } from 'child_process'
|
|
import { existsSync, readFileSync, rmSync } from 'fs'
|
|
import path from 'path'
|
|
import { type BlocksField, getPayload, type Payload } from 'payload'
|
|
import { fileURLToPath } from 'url'
|
|
|
|
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
|
|
|
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
|
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
|
import { testFilePath } from './testFilePath.js'
|
|
|
|
let restClient: NextRESTClient
|
|
let payload: Payload
|
|
|
|
const filename = fileURLToPath(import.meta.url)
|
|
const dirname = path.dirname(filename)
|
|
|
|
describe('Config', () => {
|
|
beforeAll(async () => {
|
|
;({ payload, restClient } = await initPayloadInt(dirname))
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await payload.destroy()
|
|
})
|
|
|
|
describe('payload config', () => {
|
|
it('allows a custom field at the config root', () => {
|
|
const { config } = payload
|
|
expect(config.custom).toEqual({
|
|
name: 'Customer portal',
|
|
})
|
|
})
|
|
|
|
it('allows a custom field in the root endpoints', () => {
|
|
const [endpoint] = payload.config.endpoints
|
|
|
|
expect(endpoint.custom).toEqual({
|
|
description: 'Get the sanitized payload config',
|
|
})
|
|
})
|
|
|
|
it('should allow multiple getPayload calls using different configs in same process', async () => {
|
|
const payload2 = await getPayload({
|
|
key: 'payload2',
|
|
config: await buildConfigWithDefaults({
|
|
collections: [
|
|
{
|
|
slug: 'payload2',
|
|
fields: [{ name: 'title2', type: 'text' }],
|
|
},
|
|
],
|
|
}),
|
|
})
|
|
|
|
// Use payload2 instance before creating payload3 instance, as we share the same db connection => each instance
|
|
// creation will reset the db schema.
|
|
const result2: any = await payload2.create({
|
|
collection: 'payload2',
|
|
data: {
|
|
title2: 'Payload 2',
|
|
},
|
|
} as any)
|
|
|
|
expect(result2.title2).toBe('Payload 2')
|
|
|
|
const payload3 = await getPayload({
|
|
key: 'payload3',
|
|
config: await buildConfigWithDefaults({
|
|
collections: [
|
|
{
|
|
slug: 'payload3',
|
|
fields: [{ name: 'title3', type: 'text' }],
|
|
},
|
|
],
|
|
}),
|
|
})
|
|
|
|
// If payload was still incorrectly cached, this would fail, as the old payload config would still be used
|
|
const result3: any = await payload3.create({
|
|
collection: 'payload3',
|
|
data: {
|
|
title3: 'Payload 3',
|
|
},
|
|
} as any)
|
|
|
|
expect(result3.title3).toBe('Payload 3')
|
|
|
|
await payload2.destroy()
|
|
await payload3.destroy()
|
|
})
|
|
})
|
|
|
|
describe('collection config', () => {
|
|
it('allows a custom field in collections', () => {
|
|
const [collection] = payload.config.collections
|
|
expect(collection.custom).toEqual({
|
|
externalLink: 'https://foo.bar',
|
|
})
|
|
})
|
|
|
|
it('allows a custom field in collection endpoints', () => {
|
|
const [collection] = payload.config.collections
|
|
const [endpoint] = collection.endpoints || []
|
|
|
|
expect(endpoint.custom).toEqual({
|
|
examples: [{ type: 'response', value: { message: 'hi' } }],
|
|
})
|
|
})
|
|
|
|
it('allows a custom field in collection fields', () => {
|
|
const [collection] = payload.config.collections
|
|
const [field] = collection.fields
|
|
|
|
expect(field.custom).toEqual({
|
|
description: 'The title of this page',
|
|
})
|
|
})
|
|
|
|
it('allows a custom field in blocks in collection fields', () => {
|
|
const [collection] = payload.config.collections
|
|
const [, blocksField] = collection.fields
|
|
|
|
expect((blocksField as BlocksField).blocks[0].custom).toEqual({
|
|
description: 'The blockOne of this page',
|
|
})
|
|
})
|
|
|
|
it('properly merges collection.labels with defaults', () => {
|
|
const [collection] = payload.config.collections
|
|
expect(collection?.labels).toEqual({ plural: 'Pages', singular: 'Page' })
|
|
})
|
|
})
|
|
|
|
describe('global config', () => {
|
|
it('allows a custom field in globals', () => {
|
|
const [global] = payload.config.globals
|
|
expect(global.custom).toEqual({ foo: 'bar' })
|
|
})
|
|
|
|
it('allows a custom field in global endpoints', () => {
|
|
const [global] = payload.config.globals
|
|
const [endpoint] = global.endpoints || []
|
|
|
|
expect(endpoint.custom).toEqual({ params: [{ name: 'name', type: 'string', in: 'query' }] })
|
|
})
|
|
|
|
it('allows a custom field in global fields', () => {
|
|
const [global] = payload.config.globals
|
|
const [field] = global.fields
|
|
|
|
expect(field.custom).toEqual({
|
|
description: 'The title of my global',
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('cors config', () => {
|
|
it('includes a custom header in Access-Control-Allow-Headers', async () => {
|
|
const response = await restClient.GET(`/pages`)
|
|
expect(response.headers.get('Access-Control-Allow-Headers')).toContain('x-custom-header')
|
|
})
|
|
})
|
|
|
|
describe('bin config', () => {
|
|
const executeCLI = (command: string) => {
|
|
execSync(`pnpm tsx "${path.resolve(dirname, 'bin.ts')}" ${command}`, {
|
|
env: {
|
|
...process.env,
|
|
PAYLOAD_CONFIG_PATH: path.resolve(dirname, 'config.ts'),
|
|
PAYLOAD_DROP_DATABASE: 'false',
|
|
},
|
|
stdio: 'inherit',
|
|
cwd: path.resolve(dirname, '../..'), // from root
|
|
})
|
|
}
|
|
|
|
const deleteTestFile = () => {
|
|
if (existsSync(testFilePath)) {
|
|
rmSync(testFilePath)
|
|
}
|
|
}
|
|
|
|
it.skip('should execute a custom script', () => {
|
|
deleteTestFile()
|
|
executeCLI('start-server')
|
|
expect(JSON.parse(readFileSync(testFilePath, 'utf-8')).docs).toHaveLength(1)
|
|
deleteTestFile()
|
|
})
|
|
})
|
|
})
|