diff --git a/docs/fields/array.mdx b/docs/fields/array.mdx
index eeb2769652..4e46406bad 100644
--- a/docs/fields/array.mdx
+++ b/docs/fields/array.mdx
@@ -66,7 +66,7 @@ _\* An asterisk denotes that a property is required._
## Admin Options
-The customize the appearance and behavior of the Array Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
+To customize the appearance and behavior of the Array Field in the [Admin Panel](../admin/overview), you can use the `admin` option:
```ts
import type { Field } from 'payload/types'
@@ -81,11 +81,11 @@ export const MyArrayField: Field = {
The Array Field inherits all of the default options from the base [Field Admin Config](../admin/fields#admin-options), plus the following additional options:
-| Option | Description |
-| ------------------------- | -------------------------------------------------------------------------------------------------------------------- |
-| **`initCollapsed`** | Set the initial collapsed state |
-| **`components.RowLabel`** | Function or React component to be rendered as the label on the array row. Receives `({ data, index, path })` as args |
-| **`isSortable`** | Disable order sorting by setting this value to `false` |
+| Option | Description |
+| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
+| **`initCollapsed`** | Set the initial collapsed state |
+| **`components.RowLabel`** | React component to be rendered as the label on the array row. [Example](#example-of-a-custom-rowlabel-component) |
+| **`isSortable`** | Disable order sorting by setting this value to `false` |
## Example
@@ -127,12 +127,27 @@ export const ExampleCollection: CollectionConfig = {
],
admin: {
components: {
- RowLabel: ({ data, index }) => {
- return data?.title || `Slide ${String(index).padStart(2, '0')}`
- },
+ RowLabel: '/path/to/ArrayRowLabel#ArrayRowLabel',
},
},
},
],
}
```
+
+### Example of a custom RowLabel component
+
+
+```tsx
+'use client'
+
+import { useRowLabel } from '@payloadcms/ui'
+
+export const ArrayRowLabel = () => {
+ const { data, rowNumber } = useRowLabel<{ title?: string }>()
+
+ const customLabel = `${data.title || 'Slide'} ${String(rowNumber).padStart(2, '0')} `
+
+ return
Custom Label: {customLabel}
+}
+```
diff --git a/eslint.config.js b/eslint.config.js
index 038f4347f8..ae45e1d639 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -53,6 +53,7 @@ export const rootEslintConfig = [
'payload/no-relative-monorepo-imports': 'error',
'payload/no-imports-from-exports-dir': 'error',
'payload/no-imports-from-self': 'error',
+ 'payload/proper-payload-logger-usage': 'error',
},
},
{
diff --git a/packages/eslint-plugin/customRules/proper-payload-logger-usage.js b/packages/eslint-plugin/customRules/proper-payload-logger-usage.js
new file mode 100644
index 0000000000..01817687f9
--- /dev/null
+++ b/packages/eslint-plugin/customRules/proper-payload-logger-usage.js
@@ -0,0 +1,89 @@
+export const rule = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'Disallow improper usage of payload.logger.error',
+ recommended: 'error',
+ },
+ messages: {
+ improperUsage: 'Improper logger usage. Pass { msg, err } so full error stack is logged.',
+ wrongErrorField: 'Improper usage. Use { err } instead of { error }.',
+ wrongMessageField: 'Improper usage. Use { msg } instead of { message }.',
+ },
+ schema: [],
+ },
+ create(context) {
+ return {
+ CallExpression(node) {
+ const callee = node.callee
+
+ // Function to check if the expression ends with `payload.logger.error`
+ function isPayloadLoggerError(expression) {
+ return (
+ expression.type === 'MemberExpression' &&
+ expression.property.name === 'error' && // must be `.error`
+ expression.object.type === 'MemberExpression' &&
+ expression.object.property.name === 'logger' && // must be `.logger`
+ (expression.object.object.name === 'payload' || // handles just `payload`
+ (expression.object.object.type === 'MemberExpression' &&
+ expression.object.object.property.name === 'payload')) // handles `*.payload`
+ )
+ }
+
+ // Check if the function being called is `payload.logger.error` or `*.payload.logger.error`
+ if (isPayloadLoggerError(callee)) {
+ const args = node.arguments
+
+ // Case 1: Single string is passed as the argument
+ if (
+ args.length === 1 &&
+ args[0].type === 'Literal' &&
+ typeof args[0].value === 'string'
+ ) {
+ return // Valid: single string argument
+ }
+
+ // Case 2: Object is passed as the first argument
+ if (args.length > 0 && args[0].type === 'ObjectExpression') {
+ const properties = args[0].properties
+
+ // Ensure no { error } key, only { err } is allowed
+ properties.forEach((prop) => {
+ if (prop.key.type === 'Identifier' && prop.key.name === 'error') {
+ context.report({
+ node: prop,
+ messageId: 'wrongErrorField',
+ })
+ }
+
+ // Ensure no { message } key, only { msg } is allowed
+ if (prop.key.type === 'Identifier' && prop.key.name === 'message') {
+ context.report({
+ node: prop,
+ messageId: 'wrongMessageField',
+ })
+ }
+ })
+ return // Valid object, checked for 'err'/'error' keys
+ }
+
+ // Case 3: Improper usage (string + error or additional err/error)
+ if (
+ args.length > 1 &&
+ args[0].type === 'Literal' &&
+ typeof args[0].value === 'string' &&
+ args[1].type === 'Identifier' &&
+ (args[1].name === 'err' || args[1].name === 'error')
+ ) {
+ context.report({
+ node,
+ messageId: 'improperUsage',
+ })
+ }
+ }
+ },
+ }
+ },
+}
+
+export default rule
diff --git a/packages/eslint-plugin/index.mjs b/packages/eslint-plugin/index.mjs
index dd743b0773..c2c8a03916 100644
--- a/packages/eslint-plugin/index.mjs
+++ b/packages/eslint-plugin/index.mjs
@@ -4,6 +4,7 @@ import noRelativeMonorepoImports from './customRules/no-relative-monorepo-import
import noImportsFromExportsDir from './customRules/no-imports-from-exports-dir.js'
import noFlakyAssertions from './customRules/no-flaky-assertions.js'
import noImportsFromSelf from './customRules/no-imports-from-self.js'
+import properPinoLoggerErrorUsage from './customRules/proper-payload-logger-usage.js'
/**
* @type {import('eslint').ESLint.Plugin}
@@ -11,10 +12,13 @@ import noImportsFromSelf from './customRules/no-imports-from-self.js'
const index = {
rules: {
'no-jsx-import-statements': noJsxImportStatements,
- 'no-non-retryable-assertions': noNonRetryableAssertions,
'no-relative-monorepo-imports': noRelativeMonorepoImports,
'no-imports-from-exports-dir': noImportsFromExportsDir,
'no-imports-from-self': noImportsFromSelf,
+ 'proper-payload-logger-usage': properPinoLoggerErrorUsage,
+
+ // Testing-related
+ 'no-non-retryable-assertions': noNonRetryableAssertions,
'no-flaky-assertions': noFlakyAssertions,
'no-wait-function': {
create: function (context) {
diff --git a/packages/eslint-plugin/tests/proper-payload-logger-usage.js b/packages/eslint-plugin/tests/proper-payload-logger-usage.js
new file mode 100644
index 0000000000..1eccd5123f
--- /dev/null
+++ b/packages/eslint-plugin/tests/proper-payload-logger-usage.js
@@ -0,0 +1,72 @@
+import { RuleTester } from 'eslint'
+import rule from '../customRules/proper-payload-logger-usage.js'
+
+const ruleTester = new RuleTester()
+
+// Example tests for the rule
+ruleTester.run('no-improper-payload-logger-error', rule, {
+ valid: [
+ // Valid: payload.logger.error with object containing { msg, err }
+ {
+ code: "payload.logger.error({ msg: 'some message', err })",
+ },
+ // Valid: payload.logger.error with a single string
+ {
+ code: "payload.logger.error('Some error message')",
+ },
+ // Valid: *.payload.logger.error with object
+ {
+ code: "this.payload.logger.error({ msg: 'another message', err })",
+ },
+ {
+ code: "args.req.payload.logger.error({ msg: 'different message', err })",
+ },
+ ],
+
+ invalid: [
+ // Invalid: payload.logger.error with both string and error
+ {
+ code: "payload.logger.error('Some error message', err)",
+ errors: [
+ {
+ messageId: 'improperUsage',
+ },
+ ],
+ },
+ // Invalid: *.payload.logger.error with both string and error
+ {
+ code: "this.payload.logger.error('Some error message', error)",
+ errors: [
+ {
+ messageId: 'improperUsage',
+ },
+ ],
+ },
+ {
+ code: "args.req.payload.logger.error('Some error message', err)",
+ errors: [
+ {
+ messageId: 'improperUsage',
+ },
+ ],
+ },
+ // Invalid: payload.logger.error with object containing 'message' key
+ {
+ code: "payload.logger.error({ message: 'not the right property name' })",
+ errors: [
+ {
+ messageId: 'wrongMessageField',
+ },
+ ],
+ },
+ // Invalid: *.payload.logger.error with object containing 'error' key
+ {
+ code: "this.payload.logger.error({ msg: 'another message', error })",
+ errors: [
+ {
+ messageId: 'wrongErrorField',
+ },
+ ],
+ },
+ ],
+})
diff --git a/packages/plugin-seo/src/translations/de.ts b/packages/plugin-seo/src/translations/de.ts
new file mode 100644
index 0000000000..07c738e05f
--- /dev/null
+++ b/packages/plugin-seo/src/translations/de.ts
@@ -0,0 +1,28 @@
+import type { GenericTranslationsObject } from '@payloadcms/translations'
+
+export const de: GenericTranslationsObject = {
+ $schema: './translation-schema.json',
+ 'plugin-seo': {
+ almostThere: 'Fast da',
+ autoGenerate: 'Automatisch generieren',
+ bestPractices: 'Best Practices',
+ characterCount: '{{current}}/{{minLength}}-{{maxLength}} Zeichen, ',
+ charactersLeftOver: '{{characters}} verbleiben',
+ charactersToGo: '{{characters}} übrig',
+ charactersTooMany: '{{characters}} zu viel',
+ checksPassing: '{{current}}/{{max}} Kontrollen erfolgreich',
+ good: 'Gut',
+ imageAutoGenerationTip: 'Die automatische Generierung ruft das ausgewählte Hauptbild ab.',
+ lengthTipDescription:
+ 'Diese sollte zwischen {{minLength}} und {{maxLength}} Zeichen lang sein. Für Hilfe beim Schreiben von qualitativ hochwertigen Meta-Beschreibungen siehe ',
+ lengthTipTitle:
+ 'Dieser sollte zwischen {{minLength}} und {{maxLength}} Zeichen lang sein. Für Hilfe beim Schreiben von qualitativ hochwertigen Meta-Titeln siehe ',
+ missing: 'Fehlt',
+ noImage: 'Kein Bild',
+ preview: 'Vorschau',
+ previewDescription:
+ 'Die genauen Ergebnislisten können je nach Inhalt und Suchrelevanz variieren.',
+ tooLong: 'Zu lang',
+ tooShort: 'Zu kurz',
+ },
+}
diff --git a/packages/plugin-seo/src/translations/index.ts b/packages/plugin-seo/src/translations/index.ts
index 3774a56d66..80a0fdebfa 100644
--- a/packages/plugin-seo/src/translations/index.ts
+++ b/packages/plugin-seo/src/translations/index.ts
@@ -1,5 +1,6 @@
import type { GenericTranslationsObject, NestedKeysStripped } from '@payloadcms/translations'
+import { de } from './de.js'
import { en } from './en.js'
import { es } from './es.js'
import { fa } from './fa.js'
@@ -11,6 +12,7 @@ import { ru } from './ru.js'
import { uk } from './uk.js'
export const translations = {
+ de,
en,
es,
fa,
diff --git a/packages/richtext-lexical/src/features/link/client/plugins/floatingLinkEditor/LinkEditor/index.tsx b/packages/richtext-lexical/src/features/link/client/plugins/floatingLinkEditor/LinkEditor/index.tsx
index 4750e60f15..b218bc77cb 100644
--- a/packages/richtext-lexical/src/features/link/client/plugins/floatingLinkEditor/LinkEditor/index.tsx
+++ b/packages/richtext-lexical/src/features/link/client/plugins/floatingLinkEditor/LinkEditor/index.tsx
@@ -124,7 +124,7 @@ export function LinkEditor({ anchorElem }: { anchorElem: HTMLElement }): React.R
} else {
// internal link
setLinkUrl(
- `/admin/collections/${focusLinkParent.getFields()?.doc?.relationTo}/${
+ `${config.routes.admin === '/' ? '' : config.routes.admin}/collections/${focusLinkParent.getFields()?.doc?.relationTo}/${
focusLinkParent.getFields()?.doc?.value
}`,
)
diff --git a/scripts/utils/updateChangelog.ts b/scripts/utils/updateChangelog.ts
index 371ba5e9b1..bc89e5a232 100755
--- a/scripts/utils/updateChangelog.ts
+++ b/scripts/utils/updateChangelog.ts
@@ -62,41 +62,53 @@ export const updateChangelog = async (args: Args = {}): Promise
const conventionalCommits = await getLatestCommits(fromVersion, toVersion)
- const sections: Record<'breaking' | 'feat' | 'fix' | 'perf', string[]> = {
- feat: [],
- fix: [],
- perf: [],
- breaking: [],
- }
+ type SectionKey = 'breaking' | 'feat' | 'fix' | 'perf'
- // Group commits by type
- conventionalCommits.forEach((c) => {
- if (c.isBreaking) {
- sections.breaking.push(formatCommitForChangelog(c, true))
- }
+ const sections = conventionalCommits.reduce(
+ (sections, c) => {
+ if (c.isBreaking) {
+ sections.breaking.push(c)
+ }
- if (c.type === 'feat' || c.type === 'fix' || c.type === 'perf') {
- sections[c.type].push(formatCommitForChangelog(c))
- }
+ if (['feat', 'fix', 'perf'].includes(c.type)) {
+ sections[c.type].push(c)
+ }
+ return sections
+ },
+ { feat: [], fix: [], perf: [], breaking: [] } as Record,
+ )
+
+ // Sort commits by scope, unscoped first
+ Object.values(sections).forEach((section) => {
+ section.sort((a, b) => (a.scope || '').localeCompare(b.scope || ''))
})
+ const stringifiedSections = Object.fromEntries(
+ Object.entries(sections).map(([key, commits]) => [
+ key,
+ commits.map((commit) => formatCommitForChangelog(commit, key === 'breaking')),
+ ]),
+ )
+
// Fetch commits for fromVersion to toVersion
const contributors = await createContributorSection(conventionalCommits)
const yyyyMMdd = new Date().toISOString().split('T')[0]
// Might need to swap out HEAD for the new proposed version
let changelog = `## [${proposedReleaseVersion}](https://github.com/payloadcms/payload/compare/${fromVersion}...${proposedReleaseVersion}) (${yyyyMMdd})\n\n\n`
- if (sections.feat.length) {
- changelog += `### 🚀 Features\n\n${sections.feat.join('\n')}\n\n`
+
+ // Add section headers
+ if (stringifiedSections.feat.length) {
+ changelog += `### 🚀 Features\n\n${stringifiedSections.feat.join('\n')}\n\n`
}
- if (sections.perf.length) {
- changelog += `### ⚡ Performance\n\n${sections.perf.join('\n')}\n\n`
+ if (stringifiedSections.perf.length) {
+ changelog += `### ⚡ Performance\n\n${stringifiedSections.perf.join('\n')}\n\n`
}
- if (sections.fix.length) {
- changelog += `### 🐛 Bug Fixes\n\n${sections.fix.join('\n')}\n\n`
+ if (stringifiedSections.fix.length) {
+ changelog += `### 🐛 Bug Fixes\n\n${stringifiedSections.fix.join('\n')}\n\n`
}
- if (sections.breaking.length) {
- changelog += `### ⚠️ BREAKING CHANGES\n\n${sections.breaking.join('\n')}\n\n`
+ if (stringifiedSections.breaking.length) {
+ changelog += `### ⚠️ BREAKING CHANGES\n\n${stringifiedSections.breaking.join('\n')}\n\n`
}
if (writeChangelog) {
@@ -200,11 +212,13 @@ function formatCommitForChangelog(commit: GitCommit, includeBreakingNotes = fals
if (isBreaking && includeBreakingNotes) {
// Parse breaking change notes from commit body
const [rawNotes, _] = commit.body.split('\n\n')
- let notes = rawNotes
- .split('\n')
- .map((l) => ` ${l}`) // Indent notes
- .join('\n')
- .trim()
+ let notes =
+ ` ` +
+ rawNotes
+ .split('\n')
+ .map((l) => ` ${l}`) // Indent notes
+ .join('\n')
+ .trim()
// Remove random trailing quotes that sometimes appear
if (notes.endsWith('"')) {