From ae74c65d21cd0e1cc7dbff0c104f3ea2ba4837f8 Mon Sep 17 00:00:00 2001 From: Elliot DeNolf Date: Tue, 7 Jul 2026 12:51:15 -0400 Subject: [PATCH] feat(payload): sync skill with latest Update SKILL.md and reference docs to match the upstream source in tools/claude-plugin. Add missing reference files to README structure. --- skills/payload/README.md | 5 +- skills/payload/SKILL.md | 215 +++++++++++++----- skills/payload/reference/ACCESS-CONTROL.md | 1 - skills/payload/reference/ADVANCED.md | 2 +- skills/payload/reference/COLLECTIONS.md | 33 ++- skills/payload/reference/FIELDS.md | 61 +++-- .../payload/reference/PLUGIN-DEVELOPMENT.md | 6 +- 7 files changed, 230 insertions(+), 93 deletions(-) diff --git a/skills/payload/README.md b/skills/payload/README.md index d6d9510..e0f8ffd 100644 --- a/skills/payload/README.md +++ b/skills/payload/README.md @@ -16,7 +16,7 @@ The `payload` skill provides expert guidance on: ## Usage -Once installed, the Agent will automatically invoke the skill when you're working on Payload CMS projects. The skill activates when you: +Once installed, the Agent will automatically invoke the skill when you're working on Payload projects. The skill activates when you: - Edit `payload.config.ts` files - Work with collection or global configurations @@ -36,12 +36,15 @@ skills/payload/ ├── SKILL.md # Main skill file with quick reference └── reference/ ├── FIELDS.md # All field types and configurations + ├── FIELD-TYPE-GUARDS.md # Type guards for field discrimination ├── COLLECTIONS.md # Collection patterns ├── HOOKS.md # Hook patterns and examples ├── ACCESS-CONTROL.md # Basic access control ├── ACCESS-CONTROL-ADVANCED.md # Advanced access patterns ├── QUERIES.md # Query patterns and APIs + ├── ENDPOINTS.md # Custom endpoints ├── ADAPTERS.md # Database and storage adapters + ├── PLUGIN-DEVELOPMENT.md # Plugin development patterns └── ADVANCED.md # Jobs, endpoints, localization ``` diff --git a/skills/payload/SKILL.md b/skills/payload/SKILL.md index 5abccb4..bd85a9d 100644 --- a/skills/payload/SKILL.md +++ b/skills/payload/SKILL.md @@ -9,35 +9,35 @@ Payload is a Next.js native CMS with TypeScript-first architecture, providing ad ## Quick Reference -| Task | Solution | Details | -| ------------------------ | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| Auto-generate slugs | `slugField()` | [FIELDS.md#slug-field-helper](reference/FIELDS.md#slug-field-helper) | -| Restrict content by user | Access control with query | [ACCESS-CONTROL.md#row-level-security-with-complex-queries](reference/ACCESS-CONTROL.md#row-level-security-with-complex-queries) | -| Local API user ops | `user` + `overrideAccess: false` | [QUERIES.md#access-control-in-local-api](reference/QUERIES.md#access-control-in-local-api) | -| Draft/publish workflow | `versions: { drafts: true }` | [COLLECTIONS.md#versioning--drafts](reference/COLLECTIONS.md#versioning--drafts) | -| Computed fields | `virtual: true` with afterRead | [FIELDS.md#virtual-fields](reference/FIELDS.md#virtual-fields) | -| Conditional fields | `admin.condition` | [FIELDS.md#conditional-fields](reference/FIELDS.md#conditional-fields) | -| Custom field validation | `validate` function | [FIELDS.md#text-field](reference/FIELDS.md#text-field) | -| Filter relationship list | `filterOptions` on field | [FIELDS.md#relationship](reference/FIELDS.md#relationship) | -| Select specific fields | `select` parameter | [QUERIES.md#local-api](reference/QUERIES.md#local-api) | -| Auto-set author/dates | beforeChange hook | [HOOKS.md#collection-hooks](reference/HOOKS.md#collection-hooks) | -| Prevent hook loops | `req.context` check | [HOOKS.md#hook-context](reference/HOOKS.md#hook-context) | -| Cascading deletes | beforeDelete hook | [HOOKS.md#collection-hooks](reference/HOOKS.md#collection-hooks) | -| Geospatial queries | `point` field with `near`/`within` | [FIELDS.md#point-geolocation](reference/FIELDS.md#point-geolocation) | -| Reverse relationships | `join` field type | [FIELDS.md#join-fields](reference/FIELDS.md#join-fields) | -| Next.js revalidation | Context control in afterChange | [HOOKS.md#nextjs-revalidation-with-context-control](reference/HOOKS.md#nextjs-revalidation-with-context-control) | -| Query by relationship | Nested property syntax | [QUERIES.md#nested-properties](reference/QUERIES.md#nested-properties) | -| Complex queries | AND/OR logic | [QUERIES.md#andor-logic](reference/QUERIES.md#andor-logic) | -| Transactions | Pass `req` to operations | [ADAPTERS.md#threading-req-through-operations](reference/ADAPTERS.md#threading-req-through-operations) | -| Background jobs | Jobs queue with tasks | [ADVANCED.md#jobs-queue](reference/ADVANCED.md#jobs-queue) | -| Custom API routes | Collection custom endpoints | [ADVANCED.md#custom-endpoints](reference/ADVANCED.md#custom-endpoints) | -| Cloud storage | Storage adapter plugins | [ADAPTERS.md#storage-adapters](reference/ADAPTERS.md#storage-adapters) | -| Multi-language | `localization` config + `localized: true` | [ADVANCED.md#localization](reference/ADVANCED.md#localization) | -| Create plugin | `(options) => (config) => Config` | [PLUGIN-DEVELOPMENT.md#plugin-architecture](reference/PLUGIN-DEVELOPMENT.md#plugin-architecture) | -| Plugin package setup | Package structure with SWC | [PLUGIN-DEVELOPMENT.md#plugin-package-structure](reference/PLUGIN-DEVELOPMENT.md#plugin-package-structure) | -| Add fields to collection | Map collections, spread fields | [PLUGIN-DEVELOPMENT.md#adding-fields-to-collections](reference/PLUGIN-DEVELOPMENT.md#adding-fields-to-collections) | -| Plugin hooks | Preserve existing hooks in array | [PLUGIN-DEVELOPMENT.md#adding-hooks](reference/PLUGIN-DEVELOPMENT.md#adding-hooks) | -| Check field type | Type guard functions | [FIELD-TYPE-GUARDS.md](reference/FIELD-TYPE-GUARDS.md) | +| Task | Solution | Details | +| ------------------------ | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| Auto-generate slugs | `slugField()` | [FIELDS.md#slug-field-helper](reference/FIELDS.md#slug-field-helper) | +| Restrict content by user | Access control with query | [ACCESS-CONTROL.md#row-level-security-with-complex-queries](reference/ACCESS-CONTROL.md#row-level-security-with-complex-queries) | +| Local API user ops | `user` + `overrideAccess: false` | [QUERIES.md#access-control-in-local-api](reference/QUERIES.md#access-control-in-local-api) | +| Draft/publish workflow | `versions: { drafts: true }` | [COLLECTIONS.md#versioning--drafts](reference/COLLECTIONS.md#versioning--drafts) | +| Computed fields | `virtual: true` with **field-level** `hooks.afterRead` returning the value | [FIELDS.md#virtual-fields](reference/FIELDS.md#virtual-fields) | +| Conditional fields | `admin.condition` | [FIELDS.md#conditional-fields](reference/FIELDS.md#conditional-fields) | +| Custom field validation | `validate` function | [FIELDS.md#validation](reference/FIELDS.md#validation) | +| Filter relationship list | `filterOptions` on field | [FIELDS.md#relationship](reference/FIELDS.md#relationship) | +| Select specific fields | `select` parameter | [QUERIES.md#field-selection](reference/QUERIES.md#field-selection) | +| Auto-set author/dates | beforeChange hook | [HOOKS.md#collection-hooks](reference/HOOKS.md#collection-hooks) | +| Prevent hook loops | `req.context` check | [HOOKS.md#context](reference/HOOKS.md#context) | +| Cascading deletes | beforeDelete hook | [HOOKS.md#collection-hooks](reference/HOOKS.md#collection-hooks) | +| Geospatial queries | `point` field with `near`/`within` | [FIELDS.md#point-geolocation](reference/FIELDS.md#point-geolocation) | +| Reverse relationships | `join` field type | [FIELDS.md#join-fields](reference/FIELDS.md#join-fields) | +| Next.js revalidation | Context control in afterChange | [HOOKS.md#nextjs-revalidation-with-context-control](reference/HOOKS.md#nextjs-revalidation-with-context-control) | +| Query by relationship | Nested property syntax | [QUERIES.md#nested-properties](reference/QUERIES.md#nested-properties) | +| Complex queries | AND/OR logic | [QUERIES.md#andor-logic](reference/QUERIES.md#andor-logic) | +| Transactions | Pass `req` to operations | [ADAPTERS.md#threading-req-through-operations](reference/ADAPTERS.md#threading-req-through-operations) | +| Background jobs | Jobs queue with tasks | [ADVANCED.md#jobs-queue](reference/ADVANCED.md#jobs-queue) | +| Custom API routes | Collection custom endpoints | [ADVANCED.md#custom-endpoints](reference/ADVANCED.md#custom-endpoints) | +| Cloud storage | Storage adapter plugins | [ADAPTERS.md#storage-adapters](reference/ADAPTERS.md#storage-adapters) | +| Multi-language | `localization` config + `localized: true` | [ADVANCED.md#localization](reference/ADVANCED.md#localization) | +| Create plugin | `(options) => (config) => Config` | [PLUGIN-DEVELOPMENT.md#plugin-architecture](reference/PLUGIN-DEVELOPMENT.md#plugin-architecture) | +| Plugin package setup | Package structure with SWC | [PLUGIN-DEVELOPMENT.md#plugin-package-structure](reference/PLUGIN-DEVELOPMENT.md#plugin-package-structure) | +| Add fields to collection | Map collections, spread fields | [PLUGIN-DEVELOPMENT.md#adding-fields-to-collections](reference/PLUGIN-DEVELOPMENT.md#adding-fields-to-collections) | +| Plugin hooks | Preserve existing hooks in array | [PLUGIN-DEVELOPMENT.md#adding-hooks](reference/PLUGIN-DEVELOPMENT.md#adding-hooks) | +| Check field type | Type guard functions | [FIELD-TYPE-GUARDS.md](reference/FIELD-TYPE-GUARDS.md) | ## Quick Start @@ -80,22 +80,47 @@ export default buildConfig({ ## Essential Patterns +### Defaults & Conventions + +Apply these defaults when modeling content unless there's a clear reason not to: + +- **Enable drafts/versions by default:** `versions: { drafts: true }`. This is the + recommended starting point for any content collection. It auto-injects a + `_status` field (`draft` / `published` / `changed`) — **don't add your own + `status` field**, it's redundant. Only skip versions for collections that have + no publish/draft lifecycle (e.g. internal join tables, settings). +- **Use `slugField()` for all slugs** instead of hand-rolling + `{ name: 'slug', type: 'text', unique: true }`. It auto-generates the slug from + the title, adds a regenerate toggle, and handles uniqueness/indexing for you. + It defaults to generating from a `title` field — if the collection has no + `title`, pass the source field: `slugField({ useAsSlug: 'name' })`. +- **`position: 'sidebar'` is for short, at-a-glance fields** — status, category, + author, publish date. Avoid it for long fields that need horizontal space to be + usable (description, rich text content, long text). Those belong in the main + document area. + ### Basic Collection ```ts import type { CollectionConfig } from 'payload' +import { slugField } from 'payload' export const Posts: CollectionConfig = { slug: 'posts', admin: { useAsTitle: 'title', - defaultColumns: ['title', 'author', 'status', 'createdAt'], + // _status (from versions.drafts) shows the draft/published state — no custom status field needed + defaultColumns: ['title', 'author', '_status', 'createdAt'], + }, + versions: { + drafts: true, }, fields: [ { name: 'title', type: 'text', required: true }, - { name: 'slug', type: 'text', unique: true, index: true }, - { name: 'content', type: 'richText' }, - { name: 'author', type: 'relationship', relationTo: 'users' }, + slugField(), // auto-generates from `title`, unique + indexed, sidebar position + { name: 'content', type: 'richText' }, // long field — stays in the main area, not the sidebar + // short, at-a-glance field — good sidebar candidate + { name: 'author', type: 'relationship', relationTo: 'users', admin: { position: 'sidebar' } }, ], timestamps: true, } @@ -115,8 +140,11 @@ For more collection patterns (auth, upload, drafts, live preview), see [COLLECTI // Rich text { name: 'content', type: 'richText', required: true } -// Select -{ name: 'status', type: 'select', options: ['draft', 'published'], defaultValue: 'draft' } +// Slug — use the helper instead of a hand-rolled text field +slugField() + +// Select (for genuine taxonomy — NOT publish state; use versions.drafts + _status for that) +{ name: 'category', type: 'select', options: ['news', 'tutorial', 'opinion'] } // Upload { name: 'image', type: 'upload', relationTo: 'media' } @@ -126,7 +154,10 @@ For all field types (array, blocks, point, join, virtual, conditional, etc.), se ### Hook Example +Hooks live at one of two levels and they are not interchangeable. **Collection hooks** receive `{ doc, data, req, operation, ... }` and act on the whole document. **Field hooks** live inside an individual field's `hooks` object, receive `{ value, siblingData, ... }`, and **return the new value** for that field. Computed/virtual fields, per-field formatters, and per-field access masking are field hooks; cross-field business logic is a collection hook. + ```ts +// Collection-level: business logic across the document export const Posts: CollectionConfig = { slug: 'posts', hooks: { @@ -141,8 +172,27 @@ export const Posts: CollectionConfig = { }, fields: [{ name: 'title', type: 'text' }], } + +// Field-level: compute / format a single field's value (virtual fields use this) +export const Users: CollectionConfig = { + slug: 'users', + fields: [ + { name: 'firstName', type: 'text' }, + { name: 'lastName', type: 'text' }, + { + name: 'fullName', + type: 'text', + virtual: true, + hooks: { + afterRead: [({ siblingData }) => `${siblingData.firstName} ${siblingData.lastName}`], + }, + }, + ], +} ``` +When asked to "compute a field" or "populate a field's value in a hook", use a **field-level** hook on that field — never a collection-level `afterRead` that mutates `doc`. + For all hook patterns, see [HOOKS.md](reference/HOOKS.md). For access control, see [ACCESS-CONTROL.md](reference/ACCESS-CONTROL.md). ### Access Control with Type Safety @@ -232,22 +282,6 @@ export default async function Page() { } ``` -### Logger Usage - -```ts -// ✅ Valid: single string -payload.logger.error('Something went wrong') - -// ✅ Valid: object with msg and err -payload.logger.error({ msg: 'Failed to process', err: error }) - -// ❌ Invalid: don't pass error as second argument -payload.logger.error('Failed to process', error) - -// ❌ Invalid: use `err` not `error`, use `msg` not `message` -payload.logger.error({ message: 'Failed', error: error }) -``` - ## Security Pitfalls ### 1. Local API Access Control (CRITICAL) @@ -371,21 +405,96 @@ src/ └── payload.config.ts ``` -## Type Generation +## Building & Type Generation + +Payload generates `payload-types.ts` for you — you rarely need to run `generate:types` by hand. + +- **During development:** `typescript.autoGenerate` defaults to `true`, so the dev + server regenerates types automatically whenever your config changes. Don't run + `generate:types` manually while the dev server is running — it's redundant. +- **During builds:** `payload build` generates the import map and types before + running `next build`. Prefer it over calling `next build` directly so neither is + ever stale. Pass `--no-types` to skip type generation. +- **Manual generation** (`payload generate:types`) is an escape hatch — only when + neither the dev server nor a build is in the loop (e.g. a one-off script, or CI + before a step that doesn't run `payload build`). ```ts // payload.config.ts export default buildConfig({ typescript: { outputFile: path.resolve(dirname, 'payload-types.ts'), + // autoGenerate defaults to true — types regenerate in dev automatically }, - // ... }) // Usage import type { Post, User } from '@/payload-types' ``` +## Common Gotchas + +1. **Local API bypasses access control** unless you pass `overrideAccess: false` +2. **Missing `req` in nested operations** breaks transaction atomicity +3. **Hook loops** — operations in hooks can re-trigger the same hooks; use `req.context` flags +4. **Field-level access** returns boolean only, no query constraints +5. **Relationship depth** defaults to 2; set `depth: 0` for IDs only +6. **Draft status** — `_status` field is auto-injected when drafts are enabled +7. **Types regenerate automatically** in dev (`autoGenerate`) and during `payload build` — avoid running `generate:types` manually +8. **MongoDB transactions** require replica set configuration +9. **SQLite transactions** are disabled by default; enable with `transactionOptions: {}` +10. **Point fields** are not supported in SQLite + +## Best Practices + +### Content Modeling + +- Enable `versions: { drafts: true }` by default on content collections; rely on the + auto-injected `_status` field rather than adding a custom `status` field +- Use `slugField()` for slugs instead of hand-rolling a unique text field +- Reserve `position: 'sidebar'` for short, at-a-glance fields (status, category, + author, date); keep long fields (description, rich text) in the main area + +### Security + +- Default to restrictive access, gradually add permissions +- Use `overrideAccess: false` when passing `user` to Local API +- Field-level access only returns boolean (no query constraints) +- Never trust client-provided data +- Use `saveToJWT: true` for roles to avoid database lookups + +### Performance + +- Index frequently queried fields +- Use `select` to limit returned fields +- Set `maxDepth` on relationships to prevent over-fetching +- Prefer query constraints over async operations in access control +- Cache expensive operations in `req.context` + +### Data Integrity + +- Always pass `req` to nested operations in hooks +- Use context flags to prevent infinite hook loops +- Enable transactions for MongoDB (requires replica set) and Postgres +- Use `beforeValidate` for data formatting +- Use `beforeChange` for business logic + +### Type Safety + +- Let dev (`autoGenerate`) and `payload build` generate types; run `generate:types` manually only when neither is running +- Import types from generated `payload-types.ts` +- Type your user object: `import type { User } from '@/payload-types'` +- Use field type guards for runtime type checking +- When extracting any Payload value into a named constant — a collection, field, hook, access function, plugin, etc. — annotate it with the matching Payload type (`CollectionConfig`, `Field`, `CollectionBeforeChangeHook`, `Access`, `Plugin`, …) or use `satisfies `. Without an annotation, string properties like `type: 'text'` widen to `string` and discriminated unions (`Field`, `CollectionConfig`) fail to resolve. Inline literals get this for free via contextual typing; extracted constants do not. + +### Organization + +- Keep collections in separate files +- Extract access control to `access/` directory +- Extract hooks to `hooks/` directory +- Use reusable field factories for common patterns +- Document complex access control with comments + ## Reference Documentation - **[FIELDS.md](reference/FIELDS.md)** - All field types, validation, admin options diff --git a/skills/payload/reference/ACCESS-CONTROL.md b/skills/payload/reference/ACCESS-CONTROL.md index 39571f5..d370305 100644 --- a/skills/payload/reference/ACCESS-CONTROL.md +++ b/skills/payload/reference/ACCESS-CONTROL.md @@ -85,7 +85,6 @@ export const Posts: CollectionConfig = { }, fields: [ { name: 'title', type: 'text' }, - { name: 'status', type: 'select', options: ['draft', 'published'] }, { name: 'author', type: 'relationship', relationTo: 'users' }, ], } diff --git a/skills/payload/reference/ADVANCED.md b/skills/payload/reference/ADVANCED.md index ce5e359..d744406 100644 --- a/skills/payload/reference/ADVANCED.md +++ b/skills/payload/reference/ADVANCED.md @@ -236,7 +236,7 @@ export const CustomField: TextFieldClientComponent = () => { ```tsx 'use client' -import { DefaultTemplate } from '@payloadcms/next/templates' +import { DefaultTemplate } from '@payloadcms/ui/rsc' export const CustomView = () => { return ( diff --git a/skills/payload/reference/COLLECTIONS.md b/skills/payload/reference/COLLECTIONS.md index e5a582c..abfac20 100644 --- a/skills/payload/reference/COLLECTIONS.md +++ b/skills/payload/reference/COLLECTIONS.md @@ -6,6 +6,7 @@ Complete reference for collection configurations and patterns. ```ts import type { CollectionConfig } from 'payload' +import { slugField } from 'payload' export const Posts: CollectionConfig = { slug: 'posts', @@ -15,11 +16,16 @@ export const Posts: CollectionConfig = { }, admin: { useAsTitle: 'title', - defaultColumns: ['title', 'author', 'status', 'createdAt'], + // _status comes from versions.drafts below — no custom status field needed + defaultColumns: ['title', 'author', '_status', 'createdAt'], group: 'Content', // Organize in admin sidebar description: 'Blog posts and articles', listSearchableFields: ['title', 'slug'], }, + // Enable drafts by default — auto-injects the _status field (draft/published/changed) + versions: { + drafts: true, + }, fields: [ { name: 'title', @@ -27,25 +33,18 @@ export const Posts: CollectionConfig = { required: true, index: true, }, - { - name: 'slug', - type: 'text', - unique: true, - index: true, - admin: { position: 'sidebar' }, - }, - { - name: 'status', - type: 'select', - options: ['draft', 'published'], - defaultValue: 'draft', - }, + slugField(), // unique + indexed, sidebar position — don't hand-roll a slug text field ], defaultSort: '-createdAt', timestamps: true, } ``` +> Don't add a custom `status` select for publish state — enabling +> `versions: { drafts: true }` injects a managed `_status` field +> (`draft` / `published` / `changed`) that the admin UI and Draft Preview already +> understand. Use it in `defaultColumns` and access control directly. + ## Auth Collection ```ts @@ -129,6 +128,7 @@ Enable real-time content preview during editing. ```ts import type { CollectionConfig } from 'payload' +import { slugField } from 'payload' const generatePreviewPath = ({ slug, @@ -164,10 +164,7 @@ export const Pages: CollectionConfig = { req, }), }, - fields: [ - { name: 'title', type: 'text' }, - { name: 'slug', type: 'text' }, - ], + fields: [{ name: 'title', type: 'text' }, slugField()], } ``` diff --git a/skills/payload/reference/FIELDS.md b/skills/payload/reference/FIELDS.md index 997db7d..b00ca1a 100644 --- a/skills/payload/reference/FIELDS.md +++ b/skills/payload/reference/FIELDS.md @@ -26,9 +26,19 @@ const textField: TextField = { } ``` +> **When to use `position: 'sidebar'`:** Reserve the sidebar for short fields that +> give quick insight into the content — status, category, author, publish date, +> slug. Avoid it for fields that need horizontal space to be useful, like a +> description, rich text content, or long text — those belong in the main document +> area. (`title` above is shown in the sidebar only to demonstrate the option.) + ### Slug Field Helper -Built-in helper for auto-generating slugs: +Built-in helper for auto-generating slugs. **Use this for all slugs** instead of +hand-rolling a `{ name: 'slug', type: 'text', unique: true }` field — it +auto-generates the slug from the title, adds a regenerate toggle, and handles +uniqueness and indexing. Call it with no args when the collection has a `title` +field — the slug is generated from `title` by default: ```ts import { slugField } from 'payload' @@ -38,21 +48,37 @@ export const Pages: CollectionConfig = { slug: 'pages', fields: [ { name: 'title', type: 'text', required: true }, - slugField({ - name: 'slug', // defaults to 'slug' - useAsSlug: 'title', // defaults to 'title' - checkboxName: 'generateSlug', // defaults to 'generateSlug' - localized: true, - required: true, - overrides: (defaultField) => { - // Customize the generated fields if needed - return defaultField - }, - }), + slugField(), // name: 'slug', useAsSlug: 'title', required, unique, position: 'sidebar' ], } ``` +`useAsSlug` defaults to `'title'`, so if the collection has **no `title` field**, +you must pass the source field explicitly — otherwise the slug generates from a +field that doesn't exist: + +```ts +// Collection keyed on `name` instead of `title` +fields: [{ name: 'name', type: 'text', required: true }, slugField({ useAsSlug: 'name' })] +``` + +Override defaults when needed (`overrides` receives the generated `RowField`): + +```ts +slugField({ + name: 'slug', // defaults to 'slug' + useAsSlug: 'title', // defaults to 'title' + checkboxName: 'generateSlug', // defaults to 'generateSlug' + localized: true, + required: true, // defaults to true + position: 'sidebar', // default; the slug is a short field well-suited to the sidebar + overrides: (field) => { + field.fields[1].label = 'Custom Slug Label' + return field + }, +}) +``` + ## Rich Text (Lexical) ```ts @@ -264,14 +290,17 @@ const blocksField: BlocksField = { ```ts import type { SelectField } from 'payload' +// Use select for genuine taxonomy. For publish state, enable versions.drafts +// and rely on the auto-injected _status field instead of a custom select. const selectField: SelectField = { - name: 'status', + name: 'priority', type: 'select', options: [ - { label: 'Draft', value: 'draft' }, - { label: 'Published', value: 'published' }, + { label: 'Low', value: 'low' }, + { label: 'Medium', value: 'medium' }, + { label: 'High', value: 'high' }, ], - defaultValue: 'draft', + defaultValue: 'medium', required: true, } diff --git a/skills/payload/reference/PLUGIN-DEVELOPMENT.md b/skills/payload/reference/PLUGIN-DEVELOPMENT.md index f852dd7..cd86289 100644 --- a/skills/payload/reference/PLUGIN-DEVELOPMENT.md +++ b/skills/payload/reference/PLUGIN-DEVELOPMENT.md @@ -147,15 +147,15 @@ plugin-/ "@swc/cli": "^0.1.62", "@swc/core": "^1.3.0", "copyfiles": "^2.4.1", - "cross-env": "^7.0.3", + "cross-env": "10.1.0", "eslint": "^9.0.0", "next": "^15.4.10", "payload": "^3.0.0", "react": "^19.2.1", "react-dom": "^19.2.1", "rimraf": "^5.0.0", - "typescript": "^5.0.0", - "vitest": "^3.0.0" + "typescript": "^6.0.0", + "vitest": "4.1.6" }, "peerDependencies": { "payload": "^3.0.0"