npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@consilioweb/payload-admin-ui-pro

v0.7.1

Published

Payload CMS 3 plugin — professional admin UI with 8 themes, dashboard builder, calendar/kanban/gallery views, command palette, notifications, keyboard shortcuts, collaboration presence, field enhancements, and activity audit trail

Readme

@consilioweb/payload-admin-ui-pro

Adds a themeable shell, a widget dashboard, alternative list views, a command palette, enhanced field controls and an audit trail to the Payload CMS 3 admin panel.

npm license Payload

About

Payload 3 ships an admin panel built for correctness rather than for the people who spend their day in it. This plugin adds the layer editors usually ask for next: eight themes, a widget dashboard, card/gallery/kanban/calendar list views, a ⌘K palette, richer field controls, and an audit trail that records who changed what.

Each of the six modules can be switched off independently from the plugin config, and most of the day-to-day tuning happens in a settings global inside the admin panel rather than in code. The plugin only touches config.admin, config.endpoints, collection hooks and two hidden collections of its own — your collections and data model are left alone.

Table of Contents

Features

  • 8 theme presets plus a custom accent mode, switchable live from the admin panel. Every accent is documented in the source with its contrast ratio against white (WCAG AA, 4.5:1).
  • Dashboard builder — 9 built-in widgets, drag, drop and resize with @dnd-kit, layout saved per user.
  • 5 list view modes — table, cards, gallery, kanban, calendar — auto-detected from the fields a collection actually declares.
  • Command palette (⌘K) over collections, globals, admin pages and recent documents, with a server-side search that runs as the caller.
  • Field enhancements — toggle, status badge, star rating, image preview, relationship card; opt-in per field or automatic.
  • Login branding — background, layout, welcome message, footer, favicon, title suffix.
  • Audit trail — who did what and when, field names only, never field values; notification bell, per-document timeline, version diff with restore, analytics widget and webhook rules.
  • Collaboration presence — who else has this document open.
  • Dark mode toggle and a Plugins Hub that detects the other ConsilioWEB plugins installed.
  • 7 UI languages — English, French, German, Spanish, Italian, Portuguese, Japanese — on both the client components and the Payload labels.

Installation

pnpm add @consilioweb/payload-admin-ui-pro

Peer dependencies, if your project does not already declare them:

pnpm add payload @payloadcms/ui react

Quick Start

// payload.config.ts
import { buildConfig } from 'payload'
import { adminUiProPlugin } from '@consilioweb/payload-admin-ui-pro'

export default buildConfig({
  plugins: [
    adminUiProPlugin({
      branding: {
        loginBackground: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
        welcomeMessage: 'Welcome back',
      },
      listViews: { autoDetect: true },
      fieldEnhance: { aggressive: true },
      activity: { retentionDays: 90 },
    }),
  ],
})

Then regenerate the Payload import map — this step is not optional:

pnpm payload generate:importmap

The plugin registers its components by string path (@consilioweb/payload-admin-ui-pro/client#...). Payload resolves those paths through app/(payload)/admin/importMap.js, so until that file is regenerated none of the injected components render — no error, just nothing. Re-run it after every upgrade of this package.

Everything else is configured from Settings > Admin UI Pro in the admin panel (/admin/globals/aup-settings).

Modules

Themes (8 presets)

| Theme | theme.preset id | Style | |-------|-------------------|-------| | Indigo Pro | indigo-pro | Deep indigo-violet. Professional, tech-forward. The default. | | Emerald Nature | emerald-nature | Rich green accent. Organic, clean, trustworthy. | | Slate Corporate | slate-corporate | Cool blue-gray. Minimal, enterprise, conservative. | | Amber Warm | amber-warm | Warm golden-orange accent. Energetic, creative, confident. | | Rose Soft | rose-soft | Soft pink-rose accent. Modern, elegant, premium. | | Ocean Deep | ocean-deep | Deep teal-cyan accent. Calm, focused, data-oriented. | | Crimson Bold | crimson-bold | Deep red accent. Strong, confident, media and publishing. | | Midnight Dark | midnight-dark | Ultra-dark. Neon purple on near-black, OLED-optimized. |

Use the id, never the display name: an unknown id makes getThemeById() return null, no theme CSS is emitted, and nothing warns. custom is also accepted — see theme.accent below.

The settings global also exposes a live preview and a small marketplace panel that exports the current theme as JSON and imports a pasted one, validated against the preset schema.

Dashboard

Replaces Payload's default dashboard view. Widgets are dragged, dropped and resized with @dnd-kit; the layout is persisted per user in the dashboard-preferences collection.

Built-in widgets: stats (collection counts), quick-actions, recent-activity, collection-overview, welcome, bookmarks, notes, chart (documents created over the last 30 days, SVG only) and activity-analytics (top collections, top contributors, action breakdown).

Two known gaps in the settings global's Default Widgets picker: it offers an activity-feed entry that has no matching built-in widget (the slot renders nothing), and it does not offer activity-analytics, which you can still add from the dashboard's own edit mode.

List Views (5 modes)

| Mode | Description | Auto-detected when | |------|-------------|--------------------| | Table | Payload default | Always | | Cards | Visual cards with images and badges | A title, name or subject text field, or an upload/heroImage/image/thumbnail field | | Gallery | Image grid | The collection is an upload collection | | Kanban | Drag and drop by status | A select field named status or _status | | Calendar | Monthly grid, grouped by cardConfig.subtitleField | A date field other than the system timestamps, on a non-upload collection |

A collection is only touched when auto-detection (or your explicit listViews.collections entry) yields more than the table view. The chosen mode is remembered per collection in localStorage.

The date that unlocks the calendar view and the date the grid groups on are two different things. Detection accepts any content date field, but the grid reads cardConfig.subtitleField and falls back to createdAt — and auto-detection never fills subtitleField. An auto-detected calendar therefore always groups by createdAt, whichever field triggered it; set cardConfig.subtitleField explicitly in listViews.collections to group on your own date.

The non-table views add a toolbar with saved views (name the current query, view mode and columns; stored in Payload preferences, so they follow the user across browsers) and a CSV export. The export re-fetches from the collection's REST endpoint sorted by -updatedAt: it is capped at 500 documents, keeps scalar fields only, and does not carry over the list's active filters. Cells starting with =, +, -, @, a tab or a carriage return are prefixed with an apostrophe so a spreadsheet reads them as text — a value stored through a public form must not become a formula in the exporting administrator's Excel.

Two further features live inside the views rather than in that toolbar, and neither covers all of them: inline editing of titles and status, in cards and kanban, and bulk select with a bulk-edit modal, in the cards view only — gallery, kanban and calendar have no selection. useBulkSelect, BulkActionBar and BulkEditModal are exported from ./client if you want to wire them elsewhere.

Command Palette

⌘K / Ctrl+K. Indexes collections, globals, a create shortcut per collection, the dashboard and account pages, and recent documents from the first four collections.

From three characters, it also queries GET /api/admin-ui-pro/search (debounced, rate limited to 30 requests per minute). That search is a like match on title and name across at most five collections, capped at five results per collection and fifteen in total, and it runs as the caller — documents the account cannot read are not returned.

The shortcut key and the number of recent documents come from the settings global. Only mod+k, mod+p and mod+/ are supported; anything else falls back to ⌘K.

Field Enhancements

| Field type | Enhanced into | admin.custom.enhance | Auto-applied with aggressive: true when | |------------|---------------|------------------------|-------------------------------------------| | checkbox | Toggle switch | 'toggle' | The field carries no other admin.custom.enhance value | | select | Colored badge (field + list cell) | 'badge' | Name is status, _status, state, phase or stage | | number | Star rating | 'rating' | max is 5 or 10, and min is 0 or left undeclared | | upload | Inline image preview | 'preview' | Always | | relationship | Card preview | 'card' | The field is not hasMany |

Applies to collections and globals, recursing into group, array, row and collapsible fields. Tabs are not traversed. Collections marked admin.hidden are skipped.

Branding

Injected on the login screen and on every admin page: gradient or image login background, centered or split layout, welcome message, footer text, favicon override, and an app title suffix. Brand name, logo URL and logo height are set in the settings global rather than in the plugin config.

This module also injects the favicon override and the dark mode toggle into afterNavLinks, so branding: false removes those two along with the login page — they are not independent switches.

Activity and Notifications

  • Audit trail on every tracked collection: who, what action, which collection, which document, which field names changed — never the values, and the fields listed in SENSITIVE_FIELDS (password, token, apiKey, …) are stripped.
  • Notification bell in the nav with an unread count and a dropdown feed.
  • Per-document timeline and collaboration presence, injected into the edit view's beforeDocumentControls.
  • Version diff with word-level comparison and restore, injected only on collections that have versions enabled.
  • Notification rules, configured in the settings global: match an event (create/update/delete), a collection slug (or *), and optionally a field-equals condition, then POST the action metadata to a webhook — fire-and-forget, 5 s timeout, never any field values. The in-app channel is a no-op: the notification bell already reads the activity log. The target must be https and must resolve to a public address: loopback, RFC1918, CGNAT and link-local ranges are refused, at input time and again at request time, on the initial URL and after every redirect. Add activity.webhookAllowedHosts when you deliberately notify an internal endpoint — the allowlist is honoured by both checks. A URL already stored before the plugin was upgraded is not re-judged when you save an unrelated setting (that would freeze the whole global behind one 400), but it is still refused at request time, so it never fires.

Dark Mode

Light, dark and auto (system preference), toggled from the admin nav and remembered in localStorage. The toggle is injected by the branding module, so branding: false takes it away.

Plugins Hub

A panel in the settings global that detects which other ConsilioWEB plugins are installed — by looking for their admin nav links — and links to their configuration page or their npm install command: payload-admin-nav, payload-seo-analyzer, payload-maintenance, payload-spellcheck and payload-support.

Keyboard Shortcuts

| Shortcut | Action | |----------|--------| | ⌘K | Command palette | | ⌘/ | Shortcuts help dialog | | ⌘E | Toggle sidebar | | ⌘S | Save document |

Configuration

Plugin options

| Option | Type | Default | Description | |--------|------|---------|-------------| | enabled | boolean | true | false returns the incoming config untouched. | | userCollectionSlug | string | auto-detected | Slug of the collection that authenticates admin users; both hidden collections build a relationship on it. Resolution order: this option, then admin.user, then the first collection with auth, then 'users'. Set it only when auto-detection picks the wrong one. | | dashboard | false \| DashboardModuleConfig | enabled | Widget dashboard replacing the default view. | | listViews | false \| ListViewsModuleConfig | enabled | Alternative list views. | | quickActions | false \| QuickActionsModuleConfig | enabled | Command palette and keyboard shortcuts. | | fieldEnhance | false \| FieldEnhanceModuleConfig | enabled | Enhanced field components. | | branding | false \| BrandingModuleConfig | enabled | Login page, favicon, title suffix. | | activity | false \| ActivityModuleConfig | enabled | Audit trail. | | theme.preset | string | 'indigo-pro' | Default value of the settings global's preset select. | | theme.accent | string | — | Default custom accent (HSL). Read only when the preset is custom; with any other preset the theme's own accent wins. Custom colours are validated as whole colour values — a string that could terminate its CSS declaration is refused, at input time and again before injection. | | access.settings | Access | RBAC settings: edit | Update access on the aup-settings global. | | access.permissions | (user) => Record<string, boolean \| string> \| Promise<Record<string, boolean \| string>> | — | Custom permission resolver. The exported PermissionsCallback type is narrower (AupPermissions), but this is the signature TypeScript actually enforces on the config. See Access Control. | | componentPaths | { loginBackground?, faviconInjector? } | package paths | Override component paths for symlinked monorepo development. |

Module options

| Option | Type | Default | Description | |--------|------|---------|-------------| | branding.loginBackground | string | — | Image URL or CSS gradient. | | branding.loginLayout | 'center' \| 'split' | 'center' | Login card layout. | | branding.welcomeMessage | string \| false | — | Heading on the login page. | | branding.loginFooter | string | — | Footer text on the login page. | | branding.titleSuffix | string | — | Appended to the admin document title as — <suffix>. | | listViews.autoDetect | boolean | true | Derive the available views from each collection's fields. | | listViews.collections | Record<string, CollectionViewConfig> | {} | Explicit per-collection configuration; overrides auto-detection for that slug. | | fieldEnhance.aggressive | boolean | false | Enhance eligible fields without an admin.custom.enhance opt-in. | | fieldEnhance.toggle | boolean | true | Enable the checkbox enhancement. | | fieldEnhance.statusBadge | boolean | true | Enable the select enhancement. | | fieldEnhance.rating | boolean | true | Enable the number enhancement. | | fieldEnhance.imagePreview | boolean | true | Enable the upload enhancement. | | fieldEnhance.relationCard | boolean | true | Enable the relationship enhancement. | | activity.retentionDays | number | 90 | Age cutoff for the purge. Read by the cleanup endpoint and by the scheduled job below. 0 or a negative value purges nothing — emptying the log stays a manual act. | | activity.retentionSchedule | boolean \| string | false | Runs the purge automatically as a Payload Jobs task. true schedules it daily at 03:00; a string is used as the cron expression. Off by default — see Database and updates for what switching it on adds to your schema, and what has to run the queue. | | activity.retentionQueue | string | 'default' | Queue the retention job is filed under. | | activity.collections | string[] | all | Track only these collections. | | activity.skipCollections | string[] | [] | Exclude collections, on top of the plugin's own internal ones. | | activity.webhookAllowedHosts | string[] | [] | Hostnames accepted as notification-webhook targets even though they resolve into private address space. Webhook targets are otherwise refused when they land on loopback, RFC1918, CGNAT or link-local addresses — including after a redirect. Declared in code on purpose: editing the settings global can never widen it. |

CollectionViewConfig takes views (required), defaultView, cardConfig (imageField, titleField, subtitleField, statusField, statusOptions) and kanbanConfig (statusField, columns). A calendarConfig key also type-checks, but nothing reads it — the calendar view takes its date from cardConfig.subtitleField. See Declared but not implemented.

adminUiProPlugin({
  listViews: {
    autoDetect: true,
    collections: {
      posts: {
        views: ['table', 'cards', 'kanban', 'calendar'],
        defaultView: 'cards',
        // subtitleField doubles as the date the calendar view groups on.
        cardConfig: {
          titleField: 'title',
          imageField: 'heroImage',
          statusField: '_status',
          subtitleField: 'publishedAt',
        },
        kanbanConfig: { statusField: '_status' },
      },
    },
  },
})

Declared but not implemented

These fields exist on the config types — so your editor will suggest them — but nothing in the plugin reads them. They are kept so existing configurations keep type-checking, and are listed here so nobody spends an afternoon wondering why they do nothing.

| Option | What to use instead | |--------|---------------------| | theme.sync, nav.sync | Nothing syncs with payload-admin-theme or payload-admin-nav; the Plugins Hub only detects them from the DOM. | | dashboard.widgets, dashboard.defaultLayout | Register widgets client-side with registerWidget — see Widget SDK. | | dashboard.maxWidgets | Fixed at 20 (VALIDATION_LIMITS.maxWidgets), enforced by the save endpoint. | | dashboard.allowUserCustomization | dashboardConfig.allowCustomization in the settings global. | | listViews.savedFilters | Saved views are always available on the non-table views. | | listViews.collections[slug].calendarConfig | cardConfig.subtitleField — it is the field the calendar grid actually groups on. calendarConfig is never forwarded to the view. | | quickActions.shortcut, quickActions.recentDocsCount | The commandPalette group of the settings global. | | quickActions.actions, quickActions.searchCollections | No substitute today; the palette is injected without props. | | fieldEnhance sub-toggles at runtime | The plugin config values above are the ones that apply; the settings global mirrors them for display only. | | activity.trackFields | activityConfig.trackFields in the settings global. | | branding.faviconUrl | branding.faviconUrl in the settings global. | | componentPaths.loginView | Only loginBackground and faviconInjector are read. |

Runtime vs build-time settings

| Setting | Type | Requires restart? | |---------|------|-------------------| | Theme, branding, dashboard | Runtime (settings global) | No | | List Views auto-detect | Build-time (plugin config) | Yes | | Field Enhance aggressive | Build-time (plugin config) | Yes | | Activity collections | Build-time (plugin config) | Yes |

API Endpoints

Registered on Payload's REST route (/api by default). Every endpoint but one requires a session on the collection named by admin.user — an account authenticated on another auth collection (a front-office customers, members…) gets a 403, not a 200. Those ten are rate limited per user, per the quotas below.

The exception is GET /admin-ui-pro/branding, which is public by necessity: it serves the login page, and nobody is authenticated there yet. It answers with a hand-written whitelist of nine branding values — never the settings global itself.

The limiter runs after the access decision, never before, and its counter is keyed on the caller's collection and id. Both halves matter: a caller who is refused must not be able to spend a bucket, and ids are per-collection sequences on SQLite and Postgres, so a key built on the bare id would put users#3 and customers#3 in one bucket.

| Method | Path | Access | Rate limit | Purpose | |--------|------|--------|------------|---------| | GET | /api/admin-ui-pro/branding | Public | none | The nine branding values the login page needs (logo, brand name, login background/layout/message/footer, favicon, theme preset). Positive whitelist: nothing else in the settings global can reach it. | | GET | /api/admin-ui-pro/collections | Admin collection | 300/min | Lists non-internal collections and globals for the palette and the widgets. | | GET | /api/admin-ui-pro/dashboard | Admin collection, runs as the caller | 60/min | The caller's saved widget layout. | | PATCH | /api/admin-ui-pro/dashboard | Admin collection, runs as the caller | 30/min | Saves the layout after strict size and structure validation. | | DELETE | /api/admin-ui-pro/dashboard | Admin collection, runs as the caller | 10/min | Resets the layout to the default. | | GET | /api/admin-ui-pro/search | Admin collection, runs as the caller | 30/min | like search on title/name across up to 5 collections, 15 results max. | | GET | /api/admin-ui-pro/activity | Administrators (see below) | 60/min | Paginated audit trail. Answers 403 to everyone else. | | DELETE | /api/admin-ui-pro/activity/cleanup | Admin collection and an explicit administrator role | 5/min | Deletes entries older than retentionDays. | | GET | /api/admin-ui-pro/presence | Admin collection | 120/min | Who is currently editing a given key. The key must match presence:<collection>:<id>. | | POST | /api/admin-ui-pro/presence | Admin collection | 30/min | Presence heartbeat; entries expire after 60 s. | | DELETE | /api/admin-ui-pro/presence | Admin collection | 30/min | Leaves the presence list. |

Presence is kept in memory, per server instance — it does not survive a restart and is not shared across replicas.

activity/cleanup remains stricter than the audit trail's read rule on one axis: it demands an explicit administrator role, so a host that declares no role field at all is denied rather than failing open. It now applies the admin.user collection check as well, closing the asymmetry where an account in another auth collection carrying an admin role was refused on read yet could still purge the log.

Collections and Globals

| Slug | Role | Read | Write | |------|------|------|-------| | activity-log | Audit trail entries. Hidden from the nav. | Members of the admin.user collection holding an admin/superadmin role (case-insensitive, plain strings or { value } entries). Hosts that declare no role field at all keep access. | Create and update are refused for everyone — entries are written by internal hooks and are immutable. Delete follows the same rule as read. | | dashboard-preferences | One widget layout per user. Hidden from the nav. | Own row, and only for members of the admin.user collection — ids are per-collection sequences, so an id scope alone does not separate users#3 from customers#3. | Update and delete: same rule. Create: members of the admin.user collection, and only for their own row — the posted user must be the caller. The user field also refuses to be rewritten afterwards, so a row cannot change hands; unique on user is what keeps it to one row each. | | aup-settings (global) | All admin-panel settings. Appears under Settings. | Public — the login page reads its branding before anyone signs in — except the activityConfig group, restricted to the admin.user collection because it carries webhook URLs, which are bearer credentials. | Members of the admin.user collection, then access.settings or the RBAC settings: edit permission. |

Both collection slugs are fixed. The user relationship on each points at the collection resolved from userCollectionSlug.

Package Exports

| Subpath | Exposes | Environment | |---------|---------|-------------| | . | adminUiProPlugin, createAdminUiProSettingsGlobal, createActivityLogCollection, createActivityEndpoints, createDashboardPreferencesCollection, createDashboardEndpoints, the RBAC helpers, the security validators, SENSITIVE_FIELDS, VALIDATION_LIMITS and every config type | Server — imported from payload.config.ts | | ./client | The 'use client' components (views, widgets, fields, palette, activity UI) plus registerWidget / getRegisteredWidgets and the settings/collections caches | Browser — resolved through the Payload import map | | ./views | DashboardView, BrandingConfigBridge, ListViewsConfigBridge, SettingsNavLink | React Server Components — resolved through the import map |

Requirements

| Dependency | Version | Peer | |------------|---------|------| | Node.js | ^18.20.2 \|\| >=20.9.0 | engines | | Payload CMS | ^3.79.1 | required | | @payloadcms/ui | ^3.79.1 | required — the field-enhance components import useField | | React | ^18.0.0 \|\| ^19.0.0 | required |

The floor is 3.79.1 and not 3.0.0 for two reasons. Payload releases before 3.79.1 carry a pre-authentication account takeover (GHSA-hp5w-3hxx-vmwf) and an SQL injection: a plugin range of ^3.0.0 invited a host to install one and satisfy npm. And the plugin would not have worked there anyway — it types its dashboard view with AdminViewServerProps, which Payload added in 3.24.0, and mounts the document timeline and the presence indicator in admin.components.edit.beforeDocumentControls, a slot that did not exist before 3.36.0 (on an older host those two features render nowhere, silently). Nothing in the package needs anything newer than that, so the range stays open to the whole 3.x line above the floor; development and CI run on 3.88.

Next.js is not a peer dependency: the package never imports it. The React range you can actually use is the one your @payloadcms/ui version allows — recent releases require React 19.

The package ships ESM only. require() from a CommonJS file is not supported.

@dnd-kit/core, @dnd-kit/sortable and @dnd-kit/utilities are regular dependencies and are installed for you.

Access Control

Permissions are resolved per user and consumed by the dashboard, the settings global and the branding UI.

import { resolvePermissions, hasPermission } from '@consilioweb/payload-admin-ui-pro'

Resolution order: user.aupPermissions on the user document, then access.permissions from the plugin config, then the role table below.

| Role (user.role or user.roles) | dashboard | listViews | quickActions | fieldEnhance | branding | activity | settings | |------------------------------------|-----------|-----------|--------------|--------------|----------|----------|----------| | admin, superadmin | edit | yes | yes | yes | edit | view | yes | | editor, author | view | yes | yes | yes | view | view | no | | user, viewer | view | no | no | no | no | no | no | | No authenticated user | view | no | no | no | no | no | no | | Unrecognised role, or no role field at all | edit | yes | yes | yes | edit | view | yes |

The last row is the fallback, and it is the admin preset — deliberately generous, so that a project without RBAC is not locked out of its own admin panel. Only an authenticated user ever reaches it: with no user at all the resolver returns the user preset, hence the row above. Roles are matched case-insensitively, and a roles array may hold plain strings or { value } entries.

An empty-string role is ignored in favour of the roles array, so { role: '', roles: ['editor'] } resolves to the editor preset rather than falling through to that fallback.

adminUiProPlugin({
  access: {
    // The callback receives the USER, not a { user } wrapper.
    permissions: (user) => ({
      dashboard: 'edit',
      settings: user.role === 'admin',
      activity: 'view',
    }),
  },
})

Whatever your resolver returns is merged on top of the admin preset{ ...ADMIN_PERMISSIONS, ...result }. Keys you omit therefore keep their admin value: the example above never mentions listViews, quickActions, fieldEnhance or branding, so all four stay fully granted, and permissions: () => ({ settings: false }) restricts nothing but settings. To restrict, return the complete object. The same merge applies to aupPermissions set on a user document.

The resolver runs inside a try. If it throws — for instance when written ({ user }) => …, which reads user off a user document that has no such field — the error is logged with [admin-ui-pro] and resolution falls through to the role table, whose own fallback is the admin preset. A restriction written the wrong way can therefore grant more than intended, not less: check your server logs after changing it.

Widget SDK

dashboard.widgets in the plugin config is not implemented. Register widgets from the client instead, before the dashboard mounts — an admin.components.providers entry is the usual place:

'use client'
import { registerWidget } from '@consilioweb/payload-admin-ui-pro/client'

registerWidget({
  id: 'my-custom-widget',
  name: 'My Widget',
  icon: '🎯',
  component: MyWidgetComponent,
  defaultSize: { w: 6, h: 2 },
})

Registered widgets appear next to the built-in ones in the dashboard's edit mode. Duplicate ids are ignored — the first registration wins.

Database and updates

This plugin adds collections and a global to your Payload config; it does not own your schema, and it cannot ship migrations for you. Payload resolves migrations from a single directory — the host application's — so a migration file delivered inside an npm package is never discovered.

  • In development, push (the default on the SQLite and Postgres adapters) syncs the schema for you when the plugin's collections appear.
  • In production, generate and run them yourself: payload migrate:create then payload migrate. Never push — Payload skips it as soon as NODE_ENV=production, and mixing it with a migration ledger triggers a data-loss warning.
  • Every release of this plugin states in its Upgrading section whether it changes the schema. No release has so far — verified, not assumed: scripts/dev/schema-diff.sh v0.5.0 v0.6.0 set-diffs the field name/slug/type/relationTo and unique/index/required/hasMany/ virtual declarations between two refs, and comes back empty for every tag pair since v0.4.0.

Three options make tables appear. The first two are on by default and are the plugin's own storage; the third brings Payload's job infrastructure with it:

| Option | What it adds | |--------|--------------| | activity (on by default) | activity-log. | | dashboard (on by default) | dashboard-preferences. | | activity.retentionSchedule | Payload's own payload-jobs collection, plus a payload-jobs-stats global — Payload appends both as soon as one scheduled task exists. If your app already uses Jobs, nothing new appears. |

activity.retentionSchedule also needs something to drive the queue: payload jobs:handle-schedules followed by payload jobs:run (a system cron, a platform scheduled task), or jobs.autoRun in your config. Registered without a runner, the task exists and never fires. This is why it is off by default rather than on: paying two tables for a purge that would not run is the worse trade.

Upgrading

Both released upgrades below are behaviour changes only. Neither adds, removes, renames or retypes a field, so there is nothing to migrate — see Database and updates for the one option that would change that.

0.5.x → 0.6.0

No schema change. No migration to generate.

  • POST /api/dashboard-preferences carrying a user other than the caller now returns 403. If you seed dashboard layouts for your team from a script, that write no longer passes as one of your administrators — run it through the Local API with overrideAccess: true.
  • The user relationship on dashboard-preferences is no longer writable on update, so a row cannot change hands. Rows already stored stay updatable: Payload drops the field from incoming data and falls back to the stored value.
  • The payload and @payloadcms/ui peer ranges moved from ^3.0.0 to ^3.79.1. Check the version actually installed (pnpm why payload), not the range in your package.json.

Rows planted before this release are not cleaned up by it. Run npx aup-audit-preferences once after upgrading.

0.4.x → 0.5.0

No schema change. No migration to generate.

This one is a security release, and it changes what existing data does rather than what shape it has. Two things to audit:

Values already stored are grandfathered — kept, but no longer applied. Several validators were tightened (theme colours, CSS values, webhook URLs), and each judges a value only when it is introduced or changed. That is deliberate: Payload revalidates the whole merged document on every update, so an unconditional validator would re-judge rows it never saw written and turn the settings global into a permanent 400 — you could not even change the logo. The safety net is elsewhere: an unsafe colour is dropped at render time by generateCustomCSS / generateThemeCSS, and a webhook target is re-resolved on every fire, redirects included. So nothing unsafe is applied, but the settings screen still shows the old value and will keep showing it. Open Settings → Admin UI Pro, re-save the colour fields and the webhook URLs, and they go through the current validator.

One field is not grandfathered. branding.loginBackground refuses a stored value containing url(, image-set(, @import, expression( or javascript: — the settings global will not save at all until that field is fixed, because the value renders on the anonymous login page.

Also worth checking after this upgrade: accounts authenticated on a second auth collection lose access to every /api/admin-ui-pro/* route, to dashboard-preferences and to writing the settings global; back-office roles below administrator lose read access to the activityConfig subtree; self-hosted webhook targets on private address space need activity.webhookAllowedHosts; presence keys must match presence:<collection>:<id>; and CSV exports now prefix formula-looking cells with an apostrophe.

Maintenance scripts

aup-audit-preferences

dashboard-preferences.user is a relationship to one collection, so the database stores a bare id — and ids are per-collection sequences on SQLite and Postgres, where users#3 and customers#3 are the same 3. Before the access rules were tightened, create accepted any user id from any authenticated account, and unique: true made the planted row the row the victim's dashboard reads. The fix closes that door. It does not clean up what came through it.

# read-only report; exits 2 if anything is wrong
npx aup-audit-preferences

# delete the orphan rows
npx aup-audit-preferences --fix

Run it from the root of the host application — it loads your Payload config through the payload binary and talks to your database through the Local API, so it works on SQLite, Postgres and Mongo alike. It reports two things:

  • orphans — the user id matches no document of the admin.user collection. Unreachable rows, either planted or left behind by a deleted account. --fix deletes these.
  • collisions — the id resolves in the admin collection and in another auth collection. Reported only, never deleted: on any host with two auth collections most of these are perfectly legitimate, and deleting them would destroy real layouts.

--json gives machine-readable output, --slug= targets a custom slug.

Uninstall

  1. Remove adminUiProPlugin(...) from your payload.config.ts
  2. Uninstall the package:
pnpm remove @consilioweb/payload-admin-ui-pro
  1. Regenerate the import map:
pnpm generate:importmap

Data cleanup (optional)

Nothing is removed from your database by uninstalling. What this plugin created:

| Object | Created by | |--------|-----------| | activity-log collection | the activity module | | dashboard-preferences collection | the dashboard module | | aup-settings global | always | | rows in payload-preferences with a key starting aup-saved-views- | the saved views of the list-views module |

Check the real table names before dropping anything (.tables on SQLite, \dt on Postgres). Payload derives them from the slug and a dbName override changes them, and relationship fields add _rels companions.

SQLite:

DROP TABLE IF EXISTS activity_log_rels;
DROP TABLE IF EXISTS activity_log;
DROP TABLE IF EXISTS dashboard_preferences_rels;
DROP TABLE IF EXISTS dashboard_preferences;
DROP TABLE IF EXISTS aup_settings;
DELETE FROM payload_preferences WHERE key LIKE 'aup-saved-views-%';

PostgreSQL:

DROP TABLE IF EXISTS activity_log_rels CASCADE;
DROP TABLE IF EXISTS activity_log CASCADE;
DROP TABLE IF EXISTS dashboard_preferences_rels CASCADE;
DROP TABLE IF EXISTS dashboard_preferences CASCADE;
DROP TABLE IF EXISTS aup_settings CASCADE;
DELETE FROM payload_preferences WHERE key LIKE 'aup-saved-views-%';

MongoDB:

db.getCollection('activity-log').drop()
db.getCollection('dashboard-preferences').drop()
db.getCollection('globals').deleteOne({ globalType: 'aup-settings' })
db.getCollection('payload-preferences').deleteMany({ key: /^aup-saved-views-/ })

Do not drop payload-jobs or payload-jobs-stats on the basis of this plugin: they belong to Payload, and your application may be using them for something else. They are only relevant here if you had turned activity.retentionSchedule on and nothing else in your app uses Jobs.

Support

License

MIT — ConsilioWEB