obsidian/wiki/payloadcms/admin.md
2026-05-15 15:13:56 +01:00

177 lines
6.2 KiB
Markdown

---
tags: [payloadcms, tech-patterns]
topic: payloadcms
sources: [raw/admin__overview.md, raw/admin__preferences.md, raw/admin__preview.md, raw/admin__react-hooks.md]
created: 2026-05-15
---
# PayloadCMS — Admin Panel
## Overview
- Payload auto-generates a full CRUD admin panel built on **Next.js App Router** with RSC support
- Panel lives at `app/(payload)/admin/[[...segments]]/page.tsx` — all routes are Next.js routes
- REST (`/api`) and GraphQL (`/graphql`) APIs live side-by-side in the same route group
- Fully white-label capable: custom CSS, custom React components, custom routes
- 30+ language i18n, light/dark mode, per-user preferences persisted across sessions
## Config / Setup
```ts
import { buildConfig } from 'payload'
const config = buildConfig({
admin: {
user: 'admins', // auth-enabled collection slug
dateFormat: 'dd/MM/yyyy',
theme: 'dark', // 'light' | 'dark' | 'all'
meta: { titleSuffix: '— MyApp' },
livePreview: { url: '...' },
timezones: {
defaultTimezone: 'Europe/Amsterdam',
supportedTimezones: ({ defaultTimezones }) => [
...defaultTimezones,
{ label: 'UTC', value: 'UTC' },
],
},
toast: { duration: 6000, position: 'bottom-right', limit: 3 },
routes: {
account: '/my-account',
login: '/login',
},
},
routes: {
admin: '/dashboard', // move panel off /admin
api: '/api',
},
})
```
## Key Options
| Option | Default | Notes |
|--------|---------|-------|
| `admin.user` | `'users'` | Slug of auth-enabled collection |
| `admin.theme` | `'all'` | Restrict to light / dark |
| `admin.dateFormat` | ISO | Any `date-fns` pattern |
| `admin.timezones` | — | DB always stores UTC; display only |
| `admin.toast.duration` | `4000` ms | Uses Sonner library |
| `routes.admin` | `/admin` | Moving requires updating Next.js dir structure |
### Admin-level routes (behind `/admin`)
`account`, `createFirstUser`, `forgot`, `inactivity`, `login`, `logout`, `reset`, `unauthorized`
## Preview
Preview generates a link from the admin edit view to your front-end. Different from Live Preview (iframe).
```ts
// Collection config
admin: {
preview: (doc, { req, token }) =>
doc?.enabled ? `${req.protocol}//${req.host}/${doc.slug}` : null,
}
```
### Draft Preview (Next.js)
1. Preview URL → `/preview?slug=...&previewSecret=...&path=...`
2. Route handler at `/app/preview/route.ts` verifies secret, calls `payload.auth()`, calls `draftMode().enable()`, redirects to `path`
3. Page component reads `(await draftMode()).isEnabled` and passes `draft: true` to `payload.find()`
## Preferences API
Per-user persistent preferences stored in `payload-preferences` collection.
```tsx
'use client'
import { usePreferences } from '@payloadcms/ui'
export function ColorPicker() {
const { getPreference, setPreference } = usePreferences()
useEffect(() => {
getPreference<string[]>('last-used-colors').then(setColors)
}, [getPreference])
const addColor = (color: string) =>
setPreference('last-used-colors', [...colors, color])
}
```
`getPreference(key)``Promise<T>` | `setPreference(key, value)``void`
## React Hooks (client components only)
All hooks from `@payloadcms/ui`. Require `'use client'` directive.
### Form hooks
| Hook | Purpose |
|------|---------|
| `useField({ path })` | Read/write a single field's value + state |
| `useFormFields(selector)` | Select specific fields — minimal re-renders |
| `useAllFormFields()` | Full form state + `dispatchFields` |
| `useForm()` | Form methods: `submit`, `validateForm`, `getData`, `reset`, `addFieldRow` etc. |
| `useDocumentForm()` | Top-level form — useful inside Lexical block child forms |
```tsx
'use client'
import type { TextFieldClientComponent } from 'payload'
import { useField } from '@payloadcms/ui'
export const CustomTextField: TextFieldClientComponent = ({ path }) => {
const { value, setValue } = useField<string>({ path })
return <input value={value ?? ''} onChange={e => setValue(e.target.value)} />
}
```
**Lexical gotcha:** `setValue` updates form data but does NOT re-render the editor UI. Use `dispatchFields` with both `value` and `initialValue`:
```tsx
dispatchFields({ type: 'UPDATE', path: 'body', value: newVal, initialValue: newVal })
```
### Document / collection hooks
| Hook | Returns |
|------|---------|
| `useDocumentInfo()` | `id`, `collectionSlug`, `globalSlug`, `docPermissions`, `isLocked`, `data`, `uploadStatus`, … |
| `useDocumentTitle()` | `title`, `setDocumentTitle` |
| `useListQuery()` | `data`, `query`, `handlePageChange`, `handleWhereChange`, … |
| `useSelection()` | `count`, `selected`, `toggleAll`, `totalDocs` |
### Auth / config hooks
| Hook | Returns |
|------|---------|
| `useAuth<User>()` | `user`, `logOut`, `token`, `refreshPermissions`, `permissions` |
| `useConfig()` | `config`, `getEntityConfig({ collectionSlug })` |
| `useLocale()` | `{ code, label, rtl }` |
| `useTheme()` | `theme`, `setTheme`, `autoMode` |
| `usePreferences()` | `getPreference`, `setPreference` |
### Misc hooks
- `useCollapsible()``isCollapsed`, `toggle`
- `useEditDepth()` — nesting depth in modals
- `useStepNav()` — breadcrumb control: `setStepNav(items)`
- `useDocumentEvents()` — cross-document update events
- `usePayloadAPI(url, options)` — reactive REST fetching: `[{ data, isLoading, isError }, { setParams }]`
- `useTableColumns()``setActiveColumns`, `toggleColumn`, `resetColumnsState`
- `useRouteTransition()``startRouteTransition` for programmatic navigation
## Gotchas
- `importMap.js` is auto-regenerated on startup/HMR — never edit it manually; `layout.tsx` is safe to edit
- Moving the admin panel (`routes.admin`) requires restructuring the Next.js `app/` folder to match
- Timezones are display-only; DB always stores UTC — must also enable `timezone: true` on each date field
- Multiple auth collections possible but only one can access the admin panel (`admin.user`)
- Client Components cannot receive non-serializable props from Server Components
## Related
- [[wiki/payloadcms/custom-components|Custom Components]]
- [[wiki/payloadcms/admin-panel-overview|Admin Panel Overview]]
- [[wiki/payloadcms/configuration|Configuration]]