Components, DataViews and Admin Screens
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This sub-section’s WordPress bibliography lists the reference material consulted while preparing these pages. |
Core screens such as the Site Editor’s Pages, Templates and Patterns are React applications built from three layers:
@wordpress/components (buttons, inputs, modals, notices), @wordpress/dataviews (DataViews for lists of records,
DataForm for editing one record) and, since 7.1, the WordPress Design System’s tokens and ThemeProvider. A plugin
can use the same layers for its own screens instead of a WP_List_Table subclass or a hand-built form. This page, for
plugin developers who know React (see React), builds Catalogue > Books in lumen-catalog,
the Lumen Press catalogue plugin: a list of lp_book posts with four layouts, search, filters, sorting, pagination,
a quick-edit form and a bulk "Move to trash" action, all over /wp/v2/lp_book.
This page was written against WordPress 7.1.3 (released 2026-10-06; 7.1.4, a security release of 2026-10-28, is the
version to run in production), PHP 7.4 minimum and 8.3+ recommended, @wordpress/scripts 37.0.0 on Node 24, and checked
on 2026-10-28. The screen was built with wp-scripts build and
exercised by Playwright 1.64.1 in headless Chromium, in Docker, against a 7.1.3 site on PHP 8.4 and MariaDB 11.8 (the
stack of Local Development Environments). WordPress 7.2 (in
beta, planned for 2026-12-08) is not covered. The Catalogue menu, its capability model and the Settings screen come
from Administration Menus and Settings API; the build
tooling from Block Development Environment.
Which package versions WordPress 7.1 ships
wordpress-develop pins one Gutenberg commit (the gutenberg.sha key of its package.json) and copies the built
packages from it; for 7.1.3 that is the Gutenberg 23.6 release candidate. npm has wp-6.5 to wp-7.0 dist-tags on
these packages but no wp-7.1 tag, so the versions below were read from that commit:
| Package | In 7.1.3 | Script handle in 7.1.3 |
|---|---|---|
|
37.0.0 |
|
|
17.2.0 |
none: must be bundled |
|
1.0.0 |
|
|
0.18.0 |
none: bundled (DataViews uses it internally) |
|
7.51.0, 10.51.0 |
|
|
8.3.0, React 18.3.1 |
|
@wordpress/scripts externalizes every @wordpress/* import to its wp-* handle except a list its
dependency-extraction plugin always bundles (in 37.0.0: admin-ui, dataviews, dataviews/wp, fields, grid,
icons, interface, ui, undo-manager, views and a few internals). The plugin’s own copy of DataViews is the one
that runs, whatever WordPress version hosts it, so its version is pinned. React 19 was postponed beyond 7.1 (DataViews
15.0.0 moved to it and 16.0.0 reverted); 17.x accepts ^18 || ^19.
@wordpress/components and Storybook
@wordpress/components is the UI kit of the editor and the React admin: Button, TextControl, SelectControl,
ToggleControl, ComboboxControl, Modal, Notice, SnackbarList, Card, Panel, DropdownMenu, Popover,
Tooltip, Flex, Spinner and more. The Gutenberg Storybook (https://wordpress.github.io/gutenberg/) shows each one
with editable props, including stories for DataViews and DataForm; the Block Editor Handbook’s component reference
renders the same READMEs. Exports named __experimental* or __unstable* (__experimentalHStack, …) may change
without deprecation and wp-scripts lint-js rejects them (@wordpress/no-unsafe-wp-apis), so the example uses Flex.
In 7.1 the form controls (TextControl, SelectControl, NumberControl, FormTokenField, …) are 40px high
unconditionally, so __next40pxDefaultSize (added in 6.7) has no effect on them; Button still uses it. The
Navigation component (deprecated since 6.8, replaced by Navigator) and __experimentalApplyValueToSides were
removed, and View, Flex, Divider, Surface, Truncate and Spacer no longer render through Emotion (the css
prop of View is ignored). @wordpress/ui is the newer token-based library (Card.Root, Card.Content); it has no
handle in 7.1, so a plugin that imports it bundles it.
DataViews, DataForm and DataViewsPicker
DataViews renders a dataset in a layout with search, filters, sorting, pagination, field visibility, density and
actions (per item or bulk). It is controlled: it never fetches data, it calls onChangeView with a new view and
the caller queries its source. DataForm edits one record from the same field definitions; DataViewsPicker is a
selection-oriented variant (pickerTable, pickerGrid, pickerActivity); filterSortAndPaginate() applies a view to
an in-memory array and useFormValidity() computes a DataForm validity object. Inside a plugin built with
@wordpress/scripts, import from @wordpress/dataviews/wp: a pre-built bundle that inlines its own dependencies
and imports only @wordpress/data, i18n, date, element and React from WordPress.
The view holds the user’s choices: type, search, filters ({ field, operator, value }, optionally
isLocked), page, perPage, sort, fields (visible ids in order), titleField, mediaField,
descriptionField, groupBy, infiniteScrollEnabled and a per-layout layout object. defaultLayouts lists the
layouts offered and the properties applied when the user switches to one.
| Layout | Since | layout options and use |
|---|---|---|
|
first releases (6.5 era) |
|
|
first releases |
|
|
first releases |
|
|
package 11.0.0 (WordPress 7.0) |
|
Fields have id, label, type (text, integer, number, datetime, date, media, boolean, email,
password, telephone, color, url, array), getValue/setValue, render, Edit (a control name such as
'richtext', new in 17.2.0, or a component), elements, filterBy (false or { operators, isPrimary }),
enableSorting, enableHiding, enableGlobalSearch, readOnly, isVisible and isValid (required, elements,
pattern, minLength, maxLength, min, max, custom). The type supplies default rendering, editing, sorting
and filter operators (is, isNot, contains, isAny, isNone, isAll for text; lessThan, between, … for
numbers; before, after, inThePast, … for dates); single- and multi-value operators cannot be mixed in one filter.
Actions have id, label, icon, isPrimary, isEligible( item ), supportsBulk, context and either
callback( items, { registry, onActionPerformed } ) or RenderModal (with modalHeader, modalSize,
hideModalHeader); rows are selectable only when a bulk action exists. DataForm takes data, fields, form,
validity and onChange( edits ); form.layout.type is regular, panel, card, row or details. The API moved
fast: panel came in 4.0.0, form.type became form.layout.type in 7.0.0, row came in 9.0.0, and 10.0.0 (WordPress
6.9) replaced isItemValid() with the controlled validity prop, so code written for 6.7 or 6.8 needs updating.
7.1 also lets PHP configure the Site Editor’s own DataViews screens through get_entity_view_config_posttype_page,
..._wp_template, ..._wp_template_part and ..._wp_block filters (default_view, default_layouts,
view_list, Quick Edit form, changed with $config->merge( $patch, 1 )). A plugin’s own screen passes the same
configuration as props.
The Books screen
The screen needs lp_book with 'show_in_rest' => true, the lp_genre taxonomy with show_in_rest, and the meta
keys lp_isbn, lp_pages and lp_published_on registered with show_in_rest, exactly as in
Custom Post Types and Taxonomies and
Metadata and Custom Fields. The page is visible to anyone with
edit_lp_books (Administrators and Editors); every read and write goes through the REST API with the user’s own
capabilities and the wp_rest nonce that wp-api-fetch adds, so the screen grants nothing the REST API does not.
package.json
Every package is pinned to the version in WordPress 7.1.3, so the editor’s autocompletion and the lint rules match the
runtime. Only @wordpress/dataviews and @wordpress/icons end up in the bundle; the others are externalized.
@wordpress/theme 1.0.0 declares an optional peer stylelint@^16, while @wordpress/scripts 37.0.0 installs
stylelint 17, so a plain npm install stops with ERESOLVE; the overrides entry resolves it. --legacy-peer-deps
also installs, but it skips peer dependencies such as ajv and the build then fails with MODULE_NOT_FOUND.
{
"name": "lumen-catalog",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "wp-scripts build books=./src/admin/books/index.js --output-path=build/admin",
"start": "wp-scripts start books=./src/admin/books/index.js --output-path=build/admin",
"lint:js": "wp-scripts lint-js src/admin",
"lint:style": "wp-scripts lint-style \"src/**/*.scss\""
},
"devDependencies": {
"@wordpress/scripts": "37.0.0",
"stylelint": "17.16.0"
},
"dependencies": {
"@wordpress/components": "37.0.0",
"@wordpress/core-data": "7.51.0",
"@wordpress/data": "10.51.0",
"@wordpress/dataviews": "17.2.0",
"@wordpress/element": "8.3.0",
"@wordpress/i18n": "6.24.0",
"@wordpress/icons": "15.2.0",
"@wordpress/notices": "5.51.0",
"@wordpress/theme": "1.0.0"
},
"overrides": {
"@wordpress/theme": {
"stylelint": "$stylelint"
}
}
}
The named entry books= produces books.js, books.css and books.asset.php; the block build
(wp-scripts build --blocks-manifest --experimental-modules) stays a separate script.
src/BooksScreen.php
<?php
/**
* Catalogue > Books: a React screen built with DataViews.
*
* @package Lumen\Catalog
*/
namespace Lumen\Catalog;
defined( 'ABSPATH' ) || exit;
/** Registers the submenu page and loads the built React app on it only. */
final class BooksScreen {
public const SLUG = 'lumen-catalog-books';
public const HANDLE = 'lumen-catalog-books';
/** @var string Hook suffix returned by add_submenu_page(). */
private static $hook = '';
/** Called from Plugin::boot() in admin requests. */
public static function register(): void {
// Priority 20: the parent "Catalogue" menu is added at the default priority 10.
add_action( 'admin_menu', array( self::class, 'add_page' ), 20 );
add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue' ) );
}
/** Adds Catalogue > Books, first under Overview, for anyone who can edit books. */
public static function add_page(): void {
$title = __( 'Books', 'lumen-catalog' );
self::$hook = (string) add_submenu_page( 'lumen-catalog', $title, $title, 'edit_lp_books', self::SLUG, array( self::class, 'render' ), 1 );
}
/** Prints the mount point; React replaces its content. */
public static function render(): void {
printf(
'<div class="wrap"><h1>%s</h1><div id="lumen-books-root"><p>%s</p></div></div>',
esc_html( get_admin_page_title() ),
esc_html__( 'Loading books…', 'lumen-catalog' )
);
}
/** Enqueues the bundle with the dependencies wp-scripts computed, on this screen only. */
public static function enqueue( string $hook_suffix ): void {
if ( $hook_suffix !== self::$hook ) {
return;
}
$url = plugin_dir_url( __DIR__ ) . 'build/admin/';
$asset = require plugin_dir_path( __DIR__ ) . 'build/admin/books.asset.php';
wp_enqueue_script( self::HANDLE, $url . 'books.js', $asset['dependencies'], $asset['version'], array( 'in_footer' => true ) );
wp_set_script_translations( self::HANDLE, 'lumen-catalog', plugin_dir_path( __DIR__ ) . 'languages' );
// books.css holds the bundled DataViews styles; wp-components brings the wp-theme tokens.
wp_enqueue_style( self::HANDLE, $url . 'books.css', array( 'wp-components' ), $asset['version'] );
}
}
The hook-suffix check keeps the 2 MB bundle off other screens; the wp-components style pulls in wp-theme.
src/admin/books/index.js and books.scss
/** Entry point: mounts the Books app inside the ThemeProvider of the 7.1 design system. */
import { createRoot } from '@wordpress/element';
import { ThemeProvider } from '@wordpress/theme';
import BooksApp from './app';
import './books.scss';
const container = document.getElementById( 'lumen-books-root' );
if ( container ) {
createRoot( container ).render(
<ThemeProvider color={ { primary: '#a3271f' } } cornerRadius="moderate">
<BooksApp />
</ThemeProvider>
);
}
@import "@wordpress/dataviews/build-style/style.css";
// Tokens come from the wp-theme stylesheet (a wp-components dependency).
#lumen-books-root {
margin-top: 16px;
background: var(--wpds-color-background-surface-neutral-strong);
border: var(--wpds-border-width-xs) solid var(--wpds-color-stroke-surface-neutral-weak);
border-radius: var(--wpds-border-radius-lg);
overflow: hidden;
}
The DataViews stylesheet is imported from SCSS, not from JavaScript: an
import '@wordpress/dataviews/build-style/style.css' in index.js is caught by the dependency-extraction plugin and
written into books.asset.php as a script dependency named wp-dataviews/build-style/style.css. No such handle
exists, so WordPress does not print the script at all and the screen stays on "Loading books…".
The 7.1 Design System and ThemeProvider
7.1 registers two assets named wp-theme: a stylesheet that defines every --wpds-* design token (colours,
dimensions, borders, typography, motion) at :root, and a script that exports one public React component,
ThemeProvider (from @wordpress/theme, whose index exports only ThemeProvider, its token types and private APIs).
The provider derives a full, contrast-aware colour ramp from two seeds and rewrites the tokens for its subtree:
| Prop | Effect |
|---|---|
|
Accent seed (default |
|
Background seed (default |
|
|
|
|
|
Also applies the overrides to the document root, so portalled popovers and modals inherit them. At most one per document. |
Unset props inherit from the parent provider, then from the stylesheet. Tokens cascade through the DOM, not the React
tree, so a Modal or Popover portalled outside the provider keeps the defaults unless the provider has isRoot.
Plugin CSS uses the tokens without fallback values (wp-scripts lint-style reports them as
plugin-wpds/no-token-fallback-values) and never bundles @wordpress/theme/design-tokens.css into a WordPress screen.
src/admin/books/fields.js
One set of field definitions serves both the list and the quick-edit form. Meta values live under item.meta, so the
meta fields read with getValue and write a partial record with setValue.
/** Field definitions shared by DataViews (the list) and DataForm (quick edit). */
import { __ } from '@wordpress/i18n';
const STATUSES = [
{ value: 'publish', label: __( 'Published', 'lumen-catalog' ) },
{ value: 'future', label: __( 'Scheduled', 'lumen-catalog' ) },
{ value: 'draft', label: __( 'Draft', 'lumen-catalog' ) },
{ value: 'pending', label: __( 'Pending review', 'lumen-catalog' ) },
{ value: 'private', label: __( 'Private', 'lumen-catalog' ) },
{ value: 'lp_out_of_print', label: __( 'Out of print', 'lumen-catalog' ) },
];
const NO_FILTER = { filterBy: false, enableSorting: false };
const ANY_OF = { filterBy: { operators: [ 'isAny' ] }, enableSorting: false };
const meta = ( key ) => ( {
getValue: ( { item } ) => item.meta?.[ key ],
setValue: ( { value } ) => ( { meta: { [ key ]: value } } ),
} );
export const getFields = ( genres ) => [
{
id: 'title',
type: 'text',
label: __( 'Title', 'lumen-catalog' ),
getValue: ( { item } ) => item.title?.raw ?? item.title?.rendered ?? '',
setValue: ( { value } ) => ( { title: value } ),
enableHiding: false,
filterBy: false,
isValid: { required: true },
},
{ id: 'status', type: 'text', label: __( 'Status', 'lumen-catalog' ), elements: STATUSES, ...ANY_OF },
{
id: 'lp_genre',
type: 'array',
label: __( 'Genre', 'lumen-catalog' ),
elements: genres.map( ( term ) => ( { value: term.id, label: term.name } ) ),
render: ( { item } ) =>
( item.lp_genre ?? [] ).map( ( id ) => genres.find( ( t ) => t.id === id )?.name ?? id ).join( ', ' ),
...ANY_OF,
},
{
id: 'lp_isbn',
type: 'text',
label: __( 'ISBN-13', 'lumen-catalog' ),
placeholder: '978-1-23456-789-7',
...meta( 'lp_isbn' ),
...NO_FILTER,
isValid: { pattern: '^(97[89]-[0-9]{1,5}-[0-9]{1,7}-[0-9]{1,7}-[0-9])?$' },
},
{ id: 'lp_pages', type: 'integer', label: __( 'Pages', 'lumen-catalog' ), ...meta( 'lp_pages' ), ...NO_FILTER, isValid: { min: 0, max: 5000 } },
{ id: 'lp_published_on', type: 'date', label: __( 'Publication date', 'lumen-catalog' ), ...meta( 'lp_published_on' ), ...NO_FILTER },
{ id: 'date', type: 'datetime', label: __( 'Added', 'lumen-catalog' ), filterBy: false, readOnly: true },
];
export const QUICK_EDIT_FORM = {
layout: { type: 'regular', labelPosition: 'top' },
fields: [
'title',
'status',
{ id: 'edition', label: __( 'Edition', 'lumen-catalog' ), layout: { type: 'row', alignment: 'start' }, children: [ 'lp_isbn', 'lp_pages' ] },
'lp_published_on',
],
};
Sorting and filtering are enabled only where /wp/v2/lp_book can do them server-side: orderby accepts date,
title and the other post columns but not meta keys, and the collection has status and lp_genre parameters but no
meta filters. Sorting by pages or filtering by ISBN needs a rest_lp_book_query filter that maps the parameter to a
meta_query (see REST API: Extending It). The ISBN and page limits repeat
the REST schema of the meta registration; the client check gives instant feedback and the server check stays the
authority.
src/admin/books/app.js
useEntityRecords() from @wordpress/core-data turns the query into a cached GET /wp/v2/lp_book and returns the
records with totalItems and totalPages from the X-WP-Total and X-WP-TotalPages headers. context: 'edit' gives
the raw title and requires edit_lp_books; status is listed explicitly because the default is publish only.
/** The Books screen: DataViews over /wp/v2/lp_book through the core-data store. */
import { useMemo, useState } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import { store as coreStore, useEntityRecords } from '@wordpress/core-data';
import { store as noticesStore } from '@wordpress/notices';
import { Button, Flex, SnackbarList } from '@wordpress/components';
import { DataViews } from '@wordpress/dataviews/wp';
import { __, _n, sprintf } from '@wordpress/i18n';
import { pencil, trash } from '@wordpress/icons';
import { getFields } from './fields';
import QuickEdit from './quick-edit';
const DEFAULT_VIEW = {
...{ type: 'table', search: '', page: 1, perPage: 20, filters: [], titleField: 'title' },
sort: { field: 'date', direction: 'desc' },
fields: [ 'status', 'lp_genre', 'lp_isbn', 'lp_pages', 'date' ],
};
const DEFAULT_LAYOUTS = {
table: { layout: { styles: { lp_pages: { align: 'end' } } } },
grid: { fields: [ 'lp_genre', 'lp_isbn' ], layout: { badgeFields: [ 'status' ] } },
list: { fields: [ 'status', 'lp_isbn' ] },
activity: { fields: [ 'status' ] },
};
// Translates the DataViews view into REST query arguments.
function toQuery( view ) {
const query = {
context: 'edit',
per_page: view.perPage,
page: view.page,
search: view.search || undefined,
orderby: view.sort?.field,
order: view.sort?.direction,
status: 'publish,future,draft,pending,private,lp_out_of_print',
_fields: 'id,title,status,date,meta,lp_genre',
};
for ( const { field, value } of view.filters ?? [] ) {
if ( value?.length && ( field === 'status' || field === 'lp_genre' ) ) {
query[ field ] = value.join( ',' );
}
}
return query;
}
function ConfirmTrash( { items, closeModal, onActionPerformed } ) {
const { deleteEntityRecord } = useDispatch( coreStore );
const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore );
const onConfirm = async () => {
const opts = { throwOnError: true };
try {
// Without force: true the posts go to the trash.
await Promise.all( items.map( ( item ) => deleteEntityRecord( 'postType', 'lp_book', item.id, {}, opts ) ) );
createSuccessNotice( __( 'Moved to trash.', 'lumen-catalog' ), { type: 'snackbar' } );
onActionPerformed?.( items );
} catch ( error ) {
createErrorNotice( error.message, { type: 'snackbar' } );
}
closeModal?.();
};
return (
<Flex justify="flex-end">
<Button __next40pxDefaultSize variant="tertiary" onClick={ closeModal }>{ __( 'Cancel', 'lumen-catalog' ) }</Button>
<Button __next40pxDefaultSize variant="primary" isDestructive onClick={ onConfirm }>{ __( 'Move to trash', 'lumen-catalog' ) }</Button>
</Flex>
);
}
const ACTIONS = [
{ id: 'quick-edit', label: __( 'Quick edit', 'lumen-catalog' ), icon: pencil, isPrimary: true, RenderModal: QuickEdit },
{
id: 'trash',
label: __( 'Move to trash', 'lumen-catalog' ),
icon: trash,
supportsBulk: true,
modalHeader: ( items ) =>
sprintf(
/* translators: %d: number of books. */
_n( 'Trash %d book?', 'Trash %d books?', items.length, 'lumen-catalog' ),
items.length
),
RenderModal: ConfirmTrash,
},
];
export default function BooksApp() {
const [ view, setView ] = useState( DEFAULT_VIEW );
const query = useMemo( () => toQuery( view ), [ view ] );
const { records, isResolving, totalItems, totalPages } = useEntityRecords( 'postType', 'lp_book', query );
const { records: genres } = useEntityRecords( 'taxonomy', 'lp_genre', { per_page: 100, _fields: 'id,name' } );
const fields = useMemo( () => getFields( genres ?? [] ), [ genres ] );
const notices = useSelect( ( select ) => select( noticesStore ).getNotices(), [] );
const { removeNotice } = useDispatch( noticesStore );
return (
<>
<DataViews
data={ records ?? [] }
fields={ fields }
view={ view }
onChangeView={ setView }
actions={ ACTIONS }
defaultLayouts={ DEFAULT_LAYOUTS }
isLoading={ isResolving }
paginationInfo={ { totalItems: totalItems ?? 0, totalPages: totalPages ?? 0 } }
getItemId={ ( item ) => String( item.id ) }
searchLabel={ __( 'Search books', 'lumen-catalog' ) }
empty={ <p>{ __( 'No books match these filters.', 'lumen-catalog' ) }</p> }
/>
<SnackbarList notices={ notices.filter( ( n ) => n.type === 'snackbar' ) } onRemove={ removeNotice } />
</>
);
}
Source: Block Editor Handbook: @wordpress/core-data, Block Editor Handbook: Component Reference, WordPress Developer Blog: Using Data Views to display and interact with data in plugins
deleteEntityRecord() without force moves the post to the trash (wp-api-fetch sends the DELETE as a POST with
an X-HTTP-Method-Override header). The view lives in component state, so it resets on reload; persisting it per
user means saving the view object in user meta or the preferences store and passing onReset so DataViews shows its
"Reset view" control.
src/admin/books/quick-edit.js
/** Quick edit: a DataForm in the action modal, saved with saveEntityRecord(). */
import { useMemo, useState } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { store as noticesStore } from '@wordpress/notices';
import { Button, Flex } from '@wordpress/components';
import { DataForm, useFormValidity } from '@wordpress/dataviews/wp';
import { __ } from '@wordpress/i18n';
import { getFields, QUICK_EDIT_FORM } from './fields';
const FIELDS = getFields( [] );
export default function QuickEdit( { items: [ book ], closeModal, onActionPerformed } ) {
const [ edits, setEdits ] = useState( {} );
const [ isSaving, setIsSaving ] = useState( false );
const { saveEntityRecord } = useDispatch( coreStore );
const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore );
const canEdit = useSelect(
( select ) => select( coreStore ).canUser( 'update', { kind: 'postType', name: 'lp_book', id: book.id } ),
[ book.id ]
);
// Meta edits arrive one key at a time; merge them so none is lost.
const merge = ( a, b ) => ( { ...a, ...b, meta: { ...a.meta, ...b.meta } } );
const data = useMemo( () => merge( book, edits ), [ book, edits ] );
const { validity, isValid } = useFormValidity( data, FIELDS, QUICK_EDIT_FORM );
const onSubmit = async ( event ) => {
event.preventDefault();
setIsSaving( true );
try {
await saveEntityRecord( 'postType', 'lp_book', { id: book.id, ...edits }, { throwOnError: true } );
createSuccessNotice( __( 'Book updated.', 'lumen-catalog' ), { type: 'snackbar' } );
onActionPerformed?.( [ book ] );
closeModal?.();
} catch ( error ) {
createErrorNotice( error.message, { type: 'snackbar' } );
setIsSaving( false );
}
};
return (
<form onSubmit={ onSubmit }>
<DataForm
data={ data }
fields={ FIELDS }
form={ QUICK_EDIT_FORM }
validity={ validity }
onChange={ ( change ) => setEdits( ( prev ) => merge( prev, change ) ) }
/>
<Flex justify="flex-end" style={ { marginTop: 24 } }>
<Button __next40pxDefaultSize variant="tertiary" onClick={ closeModal }>{ __( 'Cancel', 'lumen-catalog' ) }</Button>
<Button __next40pxDefaultSize variant="primary" type="submit" isBusy={ isSaving } accessibleWhenDisabled
disabled={ ! isValid || ! canEdit || isSaving || ! Object.keys( edits ).length }>
{ __( 'Save', 'lumen-catalog' ) }
</Button>
</Flex>
</form>
);
}
saveEntityRecord() sends only the changed keys, so the request is POST /wp/v2/lp_book/<id> with, for example,
{"meta":{"lp_isbn":"978-1-23456-792-7","lp_pages":288}}. canUser() issues an OPTIONS request and reads the
Allow header; the meta auth_callback and the REST schema still run on the server.
Building and checking the screen
The listings above are compact; npm run lint:js -- --fix reflows them to the Prettier style of wp-scripts, after
which lint:js and lint:style report no problems, and npm run build ends with "webpack 5.112.2 compiled with 3
warnings": webpack’s 244 KiB performance hints. books.js is 2.0 MB minified (about 390 KB gzipped) because it
carries the DataViews bundle with its own components, date picker and @wordpress/ui; books.css and books-rtl.css
are 88 KB each. The generated books.asset.php lists react, react-dom, react-jsx-runtime, wp-components,
wp-core-data, wp-data, wp-date, wp-element, wp-hooks, wp-i18n, wp-notices, wp-primitives and
wp-theme, all registered by 7.1.3.
A Playwright 1.64.1 script (headless Chromium in a node:24-slim container on the site’s Docker network) logged in as
maria.editor with five books seeded by WP-CLI and used the screen as an editor would. The menu showed Overview and
Books; the table showed Title, Status, Genre (names, not IDs), ISBN-13, Pages and Added; searching "Lighthouse" and
filtering Genre is any of "Crime fiction" each left "The Quiet Lighthouse" and "The Lighthouse Ledger"; sorting by
Title worked; the Layout menu switched to dataviews-view-grid, -list, -activity and back to -table. The brand
token --wpds-color-background-interactive-brand-strong read #3858e9 on the page and #a3271f inside the provider.
In Quick edit, the ISBN 123 left Save with aria-disabled="true"; a valid ISBN and 288 pages saved with the snackbar
"Book updated."; the bulk action opened "Trash 1 book?" and removed "Salt and Ember" from the list. The requests were:
GET /wp-json/wp/v2/lp_book?context=edit&per_page=20&page=1&orderby=date&order=desc&status=publish,future,draft,pending,private,lp_out_of_print&_fields=id,title,status,date,meta,lp_genre
GET /wp-json/wp/v2/lp_book?...&search=Lighthouse...
GET /wp-json/wp/v2/lp_book?...&lp_genre=3
GET /wp-json/wp/v2/lp_book?...&orderby=title&order=asc...
OPTIONS /wp-json/wp/v2/lp_book/9
POST /wp-json/wp/v2/lp_book/9
POST /wp-json/wp/v2/lp_book/8
Afterwards wp post meta get 9 lp_pages printed 288 and "Salt and Ember" was in the trash. As tom.author (no
edit_lp_books) the Catalogue menu was absent and the URL returned 403 "Sorry, you are not allowed to access this
page.". Browser end-to-end testing is covered in E2E Testing in the Browser and
Testing.
Common errors and pitfalls
| Symptom | Cause and fix |
|---|---|
The screen stays on "Loading books…" with no JavaScript error |
A dependency in |
Console: "Store "core/rich-text" is already registered." |
The DataViews 17.2.0 bundle carries its own |
|
Peer range of |
Modals keep the default blue |
The modal is portalled outside the |
Where older books are outdated
| Older advice | Current state |
|---|---|
Admin list screens built by subclassing |
The class is still marked private in the Code Reference and may change; React screens with DataViews are the direction core takes (Site Editor screens, 7.1 view-config filters). |
Admin pages written as PHP forms enhanced with jQuery, using a bare |
jQuery runs in noConflict mode; new screens are React apps that use |
|
|
Bundling React or |
|
Styling admin screens with hard-coded colours |
Use |
References
-
wordpress-develop 7.1.3: package.json (pinned Gutenberg commit)
-
WordPress Developer Blog: Using Data Views to display and interact with data in plugins
-
WordPress Developer Blog: How to use DataForm to create plugin settings pages
-
Make WordPress Core: Filtering Site Editor Screens in WordPress 7.1
-
Make WordPress Core: Editor components updates in WordPress 7.1