A declarative system for registering WordPress admin tables with BerlinDB integration. Eliminates hundreds of lines of boilerplate code for list tables.
composer require arraypress/wp-register-tablesregister_admin_table( 'my_orders', [
// Menu registration
'page_title' => __( 'Orders', 'myplugin' ),
'menu_title' => __( 'Orders', 'myplugin' ),
'menu_slug' => 'my-orders',
'capability' => 'manage_options',
'icon' => 'dashicons-cart',
'position' => 30,
// Labels
'labels' => [
'singular' => __( 'order', 'myplugin' ),
'plural' => __( 'orders', 'myplugin' ),
],
// Data
'callbacks' => [
'get_items' => '\\MyPlugin\\get_orders',
'get_counts' => '\\MyPlugin\\get_order_counts',
'delete' => '\\MyPlugin\\delete_order',
],
// Columns
'columns' => [
'order_number' => __( 'Order', 'myplugin' ),
'customer' => __( 'Customer', 'myplugin' ),
'total' => __( 'Total', 'myplugin' ),
'status' => __( 'Status', 'myplugin' ),
'created_at' => __( 'Date', 'myplugin' ),
],
'sortable' => [ 'order_number', 'total', 'created_at' ],
] );That's it — the menu page, rendering, screen options, and all admin hooks are handled automatically.
register_admin_table( 'table_id', [
// Menu Registration
'page_title' => 'Orders', // Page title tag text (auto-generated from labels.title)
'menu_title' => 'Orders', // Menu item text (falls back to page_title)
'menu_slug' => 'my-orders', // Admin page slug (falls back to table ID)
'parent_slug' => '', // Parent menu slug (empty = top-level page)
'capability' => 'manage_options', // Capability required to view page
'icon' => 'dashicons-admin-generic', // Dashicon or URL (top-level only)
'position' => null, // Menu position (top-level only)
// Labels
'labels' => [
'singular' => 'order', // Used in nonces, notices, no-items text
'plural' => 'orders', // Used in bulk nonces, search, views
'title' => 'Orders', // Page/header title (auto-generated from plural)
'add_new' => 'Add New Order', // Add button text (auto-generated from singular)
'search' => 'Search Orders', // Search box label (auto-generated from plural)
'not_found' => 'No orders yet.', // Empty state message
'not_found_search' => 'No orders found for your search.',
],
// Data callbacks
'callbacks' => [
'get_items' => callable, // Required: Returns array of items
'get_counts' => callable, // Required: Returns status counts array
'delete' => callable, // Optional: Enables auto delete row action
'update' => callable, // Optional: Update handler
'search_callback' => callable, // Optional: Custom search term resolution
],
// Display
'per_page' => 30, // Default items per page
'searchable' => true, // Show search box
'show_count' => false, // Show total count in header title
// Header
'logo' => '', // URL to logo image for EDD-style header
'header_title' => '', // Override title in header (falls back to labels title)
// Columns
'columns' => [], // Column definitions (see Columns section)
'sortable' => [], // Sortable column keys
'primary_column' => '', // Column for row actions (auto-detected)
'hidden_columns' => [], // Columns hidden by default in Screen Options
// Actions
'row_actions' => [], // Row action definitions (see Row Actions section)
'bulk_actions' => [], // Bulk action definitions (see Bulk Actions section)
// Filtering
'views' => [], // Status view tabs (see Views section)
'filters' => [], // Dropdown filters (see Filters section)
'status_styles' => [], // Status => badge type mappings for auto-formatting
// Flyout Integration
'flyouts' => [
'edit' => '', // Flyout ID for edit actions
'view' => '', // Flyout ID for view actions
],
'add_button' => '', // Add button: flyout ID, URL string, or callable
// Permissions
'capabilities' => [ // Per-action overrides (capability used as default)
'view' => '',
'edit' => '',
'delete' => '',
'bulk' => '',
],
// Help Tabs
'help' => [], // Help tab definitions (see Help Tabs section)
// Styling
'body_class' => '', // Additional CSS class added to admin body
] );The library automatically registers admin menu pages. No manual add_menu_page() or add_submenu_page() calls are
needed.
register_admin_table( 'my_orders', [
'page_title' => __( 'Orders', 'myplugin' ),
'menu_title' => __( 'Orders', 'myplugin' ),
'menu_slug' => 'my-orders',
'capability' => 'manage_options',
'icon' => 'dashicons-cart',
'position' => 30,
// ...
] );register_admin_table( 'my_orders', [
'page_title' => __( 'Orders', 'myplugin' ),
'menu_title' => __( 'Orders', 'myplugin' ),
'menu_slug' => 'my-orders',
'parent_slug' => 'my-plugin',
'capability' => 'manage_options',
// ...
] );Many menu fields are auto-generated if not provided:
menu_slugdefaults to the table IDpage_titledefaults tolabels.title(which itself defaults fromlabels.plural)menu_titledefaults topage_titlecapabilitydefaults tomanage_options
So the minimal registration for a submenu page is:
register_admin_table( 'my_orders', [
'parent_slug' => 'my-plugin',
'labels' => [
'singular' => __( 'order', 'myplugin' ),
'plural' => __( 'orders', 'myplugin' ),
],
'callbacks' => [
'get_items' => '\\MyPlugin\\get_orders',
'get_counts' => '\\MyPlugin\\get_order_counts',
],
'columns' => [
'name' => __( 'Name', 'myplugin' ),
'status' => __( 'Status', 'myplugin' ),
],
] );The library includes a modern EDD-style header with logo support. The header renders outside the WordPress .wrap div
for proper full-width styling.
register_admin_table( 'my_orders', [
'logo' => plugin_dir_url( __FILE__ ) . 'assets/logo.png',
'header_title' => 'Order Management',
'show_count' => true,
// ...
] );When show_count is enabled, the total item count displays next to the title.
'columns' => [
'name' => __( 'Name', 'myplugin' ),
'email' => __( 'Email', 'myplugin' ),
'status' => __( 'Status', 'myplugin' ),
],'columns' => [
'customer' => [
'label' => __( 'Customer', 'myplugin' ),
'primary' => true, // Row actions appear on this column
'align' => 'left', // left, center, right
'width' => '200px', // CSS width
'callback' => function( $item ) {
$avatar = get_avatar( $item->get_email(), 32 );
return $avatar . ' ' . esc_html( $item->get_display_name() );
},
],
'total' => [
'label' => __( 'Total', 'myplugin' ),
'align' => 'right',
],
],For more complex column layouts, use the structured format with before, title, after, and link:
'columns' => [
'customer' => [
'label' => __( 'Customer', 'myplugin' ),
'primary' => true,
'before' => function( $item ) {
return get_avatar( $item->get_email(), 32 );
},
'title' => function( $item ) {
return $item->get_display_name();
},
'after' => function( $item ) {
return '<br><small>' . esc_html( $item->get_email() ) . '</small>';
},
'link' => 'edit_flyout', // or 'view_flyout', callable, or URL string
],
],The link option controls how the title is linked:
| Value | Behavior |
|---|---|
'edit_flyout' |
Opens the edit flyout (requires flyouts.edit config) |
'view_flyout' |
Opens the view flyout (requires flyouts.view config) |
callable |
Called with $item, should return a URL |
string |
Used directly as URL |
Columns are automatically formatted based on naming patterns. The library detects column types by matching against exact names, prefixes, suffixes, and substrings:
| Type | Matching Patterns | Formatting |
|---|---|---|
email |
Contains email |
Mailto link |
phone |
phone, mobile, cell, fax, contains phone |
Clickable tel: link |
country |
country, country_code, suffix _country |
Flag + country name |
date |
created, updated, modified, contains _at or date |
Human time diff |
price |
Contains price, total, amount, _spent, cost, revenue, balance |
Formatted currency |
rate |
rate, discount, commission, suffix _rate |
Rate format |
percentage |
Contains percent, suffix _pct |
Percentage format |
status |
status, contains _status |
Status badge |
count |
count, limit, quantity, contains _count |
Number (∞ for -1) |
items |
items, order_items, suffix _items |
Summary with "and X others" |
user |
user, author, customer, suffix _user |
Avatar + linked name |
taxonomy |
terms, tags, categories, suffix _terms |
Linked term badges |
image |
image, avatar, thumbnail, contains _image |
Thumbnail (supports attachment IDs and URLs) |
color |
color, colour, suffix _color |
Color swatch + code |
url |
url, website, link |
Linked hostname |
boolean |
active, enabled, verified, prefix is_, has_, can_ |
Yes/No icon |
code |
code, sku, uuid, hash, suffix _code, _id, _key |
Monospace code |
duration |
elapsed, runtime, contains duration, suffix _seconds |
Human duration |
file_size |
size, bytes, contains filesize, suffix _size |
Human file size |
Some auto-formatted types accept additional configuration via the column config array:
'columns' => [
'status' => [
'label' => __( 'Status', 'myplugin' ),
'styles' => [ // Custom status => badge type mappings
'active' => 'success',
'inactive' => 'default',
'pending' => 'warning',
],
],
'avatar' => [
'label' => __( 'Avatar', 'myplugin' ),
'size' => [ 64, 64 ], // Image size as [width, height] or size name
],
'author' => [
'label' => __( 'Author', 'myplugin' ),
'avatar' => 24, // Avatar size in pixels for user type
],
'line_items' => [
'label' => __( 'Products', 'myplugin' ),
'singular' => 'product', // Singular label for items type
'plural' => 'products', // Plural label for items type
],
'tags' => [
'label' => __( 'Tags', 'myplugin' ),
'taxonomy' => 'post_tag', // Taxonomy slug for linked term admin pages
],
'attachment_size' => [
'label' => __( 'Size', 'myplugin' ),
'decimals' => 2, // Decimal places for file_size type
],
],Row actions appear on hover below the primary column.
'row_actions' => [
'view' => [
'label' => __( 'View', 'myplugin' ),
'url' => fn( $item ) => get_permalink( $item->get_id() ),
],
'archive' => [
'label' => __( 'Archive', 'myplugin' ),
'url' => fn( $item ) => admin_url( '...' ),
'confirm' => __( 'Archive this item?', 'myplugin' ),
'class' => 'archive-link',
],
],'row_actions' => [
'edit' => [
'label' => __( 'Edit', 'myplugin' ),
'flyout' => true, // Opens the flyout defined in flyouts.edit
],
],Define a handler callback and the action is automatically processed with nonce verification, capability checks, and
clean redirects:
'row_actions' => [
'toggle_status' => [
'label' => fn( $item ) => $item->get_status() === 'active'
? __( 'Deactivate', 'myplugin' )
: __( 'Activate', 'myplugin' ),
'confirm' => fn( $item ) => $item->get_status() === 'active'
? __( 'Deactivate this customer?', 'myplugin' )
: __( 'Activate this customer?', 'myplugin' ),
'handler' => function( $item_id, $config ) {
$customer = get_customer( $item_id );
if ( $customer ) {
$new_status = $customer->get_status() === 'active' ? 'inactive' : 'active';
update_customer( $item_id, [ 'status' => $new_status ] );
}
return true;
},
// Optional: custom nonce action (default: {action_key}_{singular}_{item_id})
'nonce_action' => 'toggle_customer_{id}',
// Optional: custom success/error notices
'notice' => [
'success' => __( 'Customer status updated.', 'myplugin' ),
'error' => __( 'Failed to update status.', 'myplugin' ),
],
],
],Handler return values control the redirect:
| Return | Behavior |
|---|---|
true |
Redirects with updated=1 |
false |
Redirects with error=action_failed |
array |
Array keys become URL parameters (e.g., ['activated' => 1]) |
For full control over the action HTML:
'row_actions' => [
'custom' => [
'callback' => function( $item ) {
return sprintf( '<a href="%s">%s</a>', esc_url( '...' ), 'Custom' );
},
],
],Actions can be conditionally shown based on the item:
'row_actions' => [
'refund' => [
'label' => __( 'Refund', 'myplugin' ),
'condition' => fn( $item ) => $item->get_status() === 'completed',
'handler' => function( $item_id ) { /* ... */ },
],
],Individual row actions can require specific capabilities:
'row_actions' => [
'delete_permanently' => [
'label' => __( 'Delete Permanently', 'myplugin' ),
'capability' => 'delete_others_posts',
'handler' => function( $item_id ) { /* ... */ },
],
],When you provide a delete callback in callbacks, the library automatically adds a delete row action with nonce
verification and confirmation dialog. To disable:
'callbacks' => [
'delete' => '\\MyPlugin\\delete_order',
],
// The delete row action is added automatically.
// To prevent it, simply omit the delete callback.For complete control, pass a callable instead of an array:
'row_actions' => function( $item, $item_id ) {
$actions = [];
$actions['edit'] = sprintf( '<a href="%s">Edit</a>', esc_url( '...' ) );
return $actions;
},'bulk_actions' => [
'delete' => [
'label' => __( 'Delete', 'myplugin' ),
'callback' => function( $ids ) {
$deleted = 0;
foreach ( $ids as $id ) {
if ( delete_item( $id ) ) {
$deleted++;
}
}
return [ 'deleted' => $deleted ];
},
],
'activate' => [
'label' => __( 'Set Active', 'myplugin' ),
'capability' => 'manage_options',
'callback' => function( $ids ) {
$updated = 0;
foreach ( $ids as $id ) {
if ( update_item( $id, [ 'status' => 'active' ] ) ) {
$updated++;
}
}
return [ 'updated' => $updated ];
},
'notice' => [
'success' => __( '%d customers activated.', 'myplugin' ),
'error' => __( 'Failed to activate customers.', 'myplugin' ),
],
],
],Callback return values control the redirect:
| Return | Behavior |
|---|---|
array |
Keys become URL parameters |
int |
Redirects with updated={value} |
bool |
Redirects with updated={count} or updated=0 |
The notice config supports both array and callable formats:
// Array format (with %d placeholder for count)
'notice' => [
'success' => __( '%d customers activated.', 'myplugin' ),
'error' => __( 'Failed to activate customers.', 'myplugin' ),
],
// Callable format (receives $_GET for full control)
'notice' => function( $params ) {
$count = absint( $params['updated'] ?? 0 );
return [
'type' => 'success',
'message' => sprintf( '%d items processed.', $count ),
];
},Views display as clickable tabs above the table. Counts are automatically fetched from the get_counts callback.
Keys are auto-labeled by replacing underscores/hyphens with spaces and capitalizing:
'views' => [ 'active', 'pending', 'not_active' ],
// Renders as: All | Active | Pending | Not Active'views' => [
'active' => __( 'Active', 'myplugin' ),
'pending' => __( 'Awaiting Review', 'myplugin' ),
'completed' => __( 'Completed', 'myplugin' ),
],'views' => [
'active', // Auto-labeled "Active"
'pending' => __( 'Awaiting Review', 'myplugin' ), // Custom label
'inactive', // Auto-labeled "Inactive"
],Views with zero items are automatically hidden. The "All" tab is always shown with the total count.
Dropdown filters appear above the table with a "Filter" button. A "Clear" button appears when any filter is active.
'filters' => [
'country' => [
'label' => __( 'All Countries', 'myplugin' ),
'options_callback' => fn() => get_country_options(),
],
'type' => [
'label' => __( 'All Types', 'myplugin' ),
'options' => [
'physical' => __( 'Physical', 'myplugin' ),
'digital' => __( 'Digital', 'myplugin' ),
],
],
'date_range' => [
'label' => __( 'All Dates', 'myplugin' ),
'options' => [
'today' => __( 'Today', 'myplugin' ),
'this_week' => __( 'This Week', 'myplugin' ),
'this_month' => __( 'This Month', 'myplugin' ),
],
'apply_callback' => function( &$args, $value ) {
if ( $value === 'today' ) {
$args['date_query'] = [ 'after' => 'today' ];
}
},
],
],Without an apply_callback, the filter value is passed directly as a query argument using the filter key (e.g.,
$args['country'] = 'us').
When searchable is enabled (default), a search box appears above the table. The search term is passed as
$args['search'] to the get_items callback.
For tables where the search term needs to be resolved against related data (e.g., searching orders by customer email when the email lives in a separate customers table):
'callbacks' => [
'get_items' => '\\MyPlugin\\get_orders',
'get_counts' => '\\MyPlugin\\get_order_counts',
'search_callback' => function( string $search ) {
// Look up customer by email
$customer = get_customer_by_email( $search );
if ( $customer ) {
return [ 'customer_id' => $customer->get_id() ];
}
// Search customers by name, return matching IDs
$customers = get_customers( [ 'search' => $search, 'fields' => 'ids' ] );
if ( ! empty( $customers ) ) {
return [ 'customer_id__in' => $customers ];
}
// Return empty array to fall back to default search behavior
return [];
},
],The callback receives the search string and returns an array of query args to merge into the query. When the callback
returns a non-empty array, the raw search term is not passed to get_items. When it returns an empty array, the default
$args['search'] behavior is used as a fallback.
When a search is active, a banner displays showing the search term with a "Clear search" link. This is automatic and requires no configuration.
Map status values to badge types for automatic status column formatting:
'status_styles' => [
'active' => 'success',
'pending' => 'warning',
'inactive' => 'default',
'cancelled' => 'danger',
],These styles are passed to the auto-formatter when rendering status columns.
Apply one capability to all actions (also used for menu page access):
'capability' => 'manage_options',Override capabilities for specific actions. The single capability value is used as the default for any action not
explicitly defined:
'capability' => 'edit_posts', // Default for all actions + menu access
'capabilities' => [
'view' => 'edit_posts', // View the table
'edit' => 'edit_posts', // Edit row actions
'delete' => 'delete_others_posts', // Delete row action
'bulk' => 'manage_options', // Bulk action dropdown
],The add button appears in the header area. Three formats are supported:
// Flyout ID — opens a flyout panel
'add_button' => 'customers_add',
// URL — renders as a link button
'add_button' => admin_url( 'admin.php?page=add-customer' ),
// Callable — full control over output
'add_button' => function() {
return '<a href="#" class="page-title-action">Add New</a>';
},The button text comes from labels.add_new. If add_new is empty, no button is rendered.
Integrates with wp-register-flyouts for inline editing panels:
register_admin_table( 'my_orders', [
'flyouts' => [
'edit' => 'orders_edit', // Flyout ID for editing
'view' => 'orders_view', // Flyout ID for viewing
],
'add_button' => 'orders_add', // Flyout ID for adding
'row_actions' => [
'edit' => [
'label' => __( 'Edit', 'myplugin' ),
'flyout' => true, // Uses flyouts.edit
],
],
// Structured columns can also link to flyouts
'columns' => [
'name' => [
'label' => __( 'Name', 'myplugin' ),
'title' => fn( $item ) => $item->get_name(),
'link' => 'edit_flyout', // Uses flyouts.edit
],
],
] );Add help tabs to the Screen Options area:
'help' => [
'overview' => [
'title' => __( 'Overview', 'myplugin' ),
'content' => '<p>This screen shows all customers.</p>',
],
'filters' => [
'title' => __( 'Filtering', 'myplugin' ),
'callback' => function() {
return '<p>Use the dropdowns to filter by country or status.</p>';
},
],
'sidebar' => '<p><strong>For more info:</strong></p><p><a href="#">Documentation</a></p>',
],The special sidebar key sets the help sidebar content. All other keys create individual help tabs.
The library automatically registers a "Number of items per page" screen option. Users can also show/hide columns via Screen Options. Both settings persist per-user.
The library maintains clean URLs throughout:
- Filter submissions redirect to clean URLs (no
_wpnonce,_wp_http_referer,actionin URL) - Single actions redirect to clean URLs after processing
- Bulk actions redirect to clean URLs after processing
Admin table pages automatically receive CSS body classes for targeted styling:
admin-table— added to all table pagesadmin-table-{id}— table-specific class (e.g.,admin-table-my_customers)- Custom class from the
body_classconfig option
// Modify column definitions
add_filter( 'arraypress_table_columns', fn( $columns, $id, $config ) => $columns, 10, 3 );
// Modify hidden columns
add_filter( 'arraypress_table_hidden_columns', fn( $hidden, $id, $config ) => $hidden, 10, 3 );
// Modify sortable columns
add_filter( 'arraypress_table_sortable_columns', fn( $sortable, $id, $config ) => $sortable, 10, 3 );
// Modify query args before fetching items
add_filter( 'arraypress_table_query_args', fn( $args, $id, $config ) => $args, 10, 3 );
add_filter( 'arraypress_table_query_args_{table_id}', fn( $args, $config ) => $args, 10, 2 );
// Modify row actions
add_filter( 'arraypress_table_row_actions', fn( $actions, $item, $id ) => $actions, 10, 3 );
add_filter( 'arraypress_table_row_actions_{table_id}', fn( $actions, $item ) => $actions, 10, 2 );
// Modify bulk actions
add_filter( 'arraypress_table_bulk_actions', fn( $actions, $id ) => $actions, 10, 2 );
// Modify status views
add_filter( 'arraypress_table_views', fn( $views, $id, $status ) => $views, 10, 3 );
// Custom admin notices
add_filter( 'arraypress_table_admin_notices', fn( $notices, $id, $config ) => $notices, 10, 3 );
add_filter( 'arraypress_table_admin_notices_{table_id}', fn( $notices, $config ) => $notices, 10, 2 );// Before/after table renders
add_action( 'arraypress_before_render_table', fn( $id, $config ) => null, 10, 2 );
add_action( 'arraypress_before_render_table_{table_id}', fn( $config ) => null, 10, 1 );
add_action( 'arraypress_after_render_table', fn( $id, $config ) => null, 10, 2 );
add_action( 'arraypress_after_render_table_{table_id}', fn( $config ) => null, 10, 1 );
// Item deleted
add_action( 'arraypress_table_item_deleted', fn( $item_id, $result, $id, $config ) => null, 10, 4 );
add_action( 'arraypress_table_item_deleted_{table_id}', fn( $item_id, $result, $config ) => null, 10, 3 );
// Bulk action processed
add_action( 'arraypress_table_bulk_action', fn( $items, $action, $id ) => null, 10, 3 );
add_action( 'arraypress_table_bulk_action_{table_id}', fn( $items, $action ) => null, 10, 2 );
add_action( 'arraypress_table_bulk_action_{table_id}_{action}', fn( $items ) => null, 10, 1 );
// Custom single action (only needed if NOT using handler in row_actions config)
add_action( 'arraypress_table_single_action_{table_id}', fn( $action, $item_id, $config ) => null, 10, 3 );register_admin_table( 'my_customers', [
// Menu registration
'page_title' => __( 'Customers', 'myplugin' ),
'menu_title' => __( 'Customers', 'myplugin' ),
'menu_slug' => 'my-customers',
'capability' => 'manage_options',
'icon' => 'dashicons-groups',
'position' => 30,
// Labels
'labels' => [
'singular' => __( 'customer', 'myplugin' ),
'plural' => __( 'customers', 'myplugin' ),
'title' => __( 'Customers', 'myplugin' ),
],
// Data
'callbacks' => [
'get_items' => '\\MyPlugin\\get_customers',
'get_counts' => '\\MyPlugin\\get_customer_counts',
'delete' => '\\MyPlugin\\delete_customer',
'update' => '\\MyPlugin\\update_customer',
'search_callback' => function( string $search ) {
$order_customer_ids = get_order_customer_ids_by_search( $search );
if ( ! empty( $order_customer_ids ) ) {
return [ 'id__in' => $order_customer_ids ];
}
return [];
},
],
// Display
'logo' => plugin_dir_url( __FILE__ ) . 'logo.png',
'per_page' => 25,
'show_count' => true,
'body_class' => 'customers-page',
// Flyouts
'flyouts' => [
'edit' => 'customers_edit',
],
'add_button' => 'customers_add',
// Columns
'columns' => [
'name' => [
'label' => __( 'Customer', 'myplugin' ),
'primary' => true,
'before' => function( $item ) {
return get_avatar( $item->get_email(), 32 );
},
'title' => function( $item ) {
return $item->get_name();
},
'link' => 'edit_flyout',
],
'email' => __( 'Email', 'myplugin' ),
'total_spent' => [
'label' => __( 'Total Spent', 'myplugin' ),
'align' => 'right',
],
'status' => [
'label' => __( 'Status', 'myplugin' ),
'width' => '100px',
],
'country' => __( 'Country', 'myplugin' ),
'date_created' => __( 'Joined', 'myplugin' ),
],
'sortable' => [ 'name', 'total_spent', 'date_created' ],
// Row Actions
'row_actions' => [
'edit' => [
'label' => __( 'Edit', 'myplugin' ),
'flyout' => true,
],
'toggle_status' => [
'label' => fn( $item ) => $item->get_status() === 'active'
? __( 'Deactivate', 'myplugin' )
: __( 'Activate', 'myplugin' ),
'confirm' => fn( $item ) => $item->get_status() === 'active'
? __( 'Deactivate this customer?', 'myplugin' )
: __( 'Activate this customer?', 'myplugin' ),
'handler' => function( $item_id ) {
$customer = get_customer( $item_id );
$new_status = $customer->get_status() === 'active' ? 'inactive' : 'active';
return update_customer( $item_id, [ 'status' => $new_status ] );
},
'notice' => function( $params ) {
$action = isset( $params['activated'] ) ? 'activated' : 'deactivated';
return [
'type' => 'success',
'message' => sprintf( 'Customer %s successfully.', $action ),
];
},
],
],
// Bulk Actions
'bulk_actions' => [
'delete' => [
'label' => __( 'Delete', 'myplugin' ),
'callback' => function( $ids ) {
$deleted = 0;
foreach ( $ids as $id ) {
if ( delete_customer( $id ) ) {
$deleted++;
}
}
return [ 'deleted' => $deleted ];
},
],
'activate' => [
'label' => __( 'Set Active', 'myplugin' ),
'capability' => 'manage_options',
'callback' => function( $ids ) {
$updated = 0;
foreach ( $ids as $id ) {
if ( update_customer( $id, [ 'status' => 'active' ] ) ) {
$updated++;
}
}
return [ 'updated' => $updated ];
},
'notice' => [
'success' => __( '%d customers activated.', 'myplugin' ),
'error' => __( 'Failed to activate customers.', 'myplugin' ),
],
],
],
// Views & Filters
'views' => [ 'active', 'inactive', 'pending' ],
'filters' => [
'country' => [
'label' => __( 'All Countries', 'myplugin' ),
'options_callback' => '\\MyPlugin\\get_country_options',
],
],
'status_styles' => [
'active' => 'success',
'inactive' => 'default',
'pending' => 'warning',
],
'capabilities' => [
'delete' => 'manage_options',
],
'help' => [
'overview' => [
'title' => __( 'Overview', 'myplugin' ),
'content' => '<p>Manage your customers from this screen.</p>',
],
],
] );If you were previously using get_table_renderer() with manual menu registration, you can now remove that boilerplate:
Before (v1):
register_admin_table( 'my_customers', [
'page' => 'my-customers',
// ...
] );
add_action( 'admin_menu', function() {
add_menu_page(
'Customers',
'Customers',
'manage_options',
'my-customers',
get_table_renderer( 'my_customers' ),
'dashicons-groups',
30
);
} );After (v2):
register_admin_table( 'my_customers', [
'menu_slug' => 'my-customers',
'capability' => 'manage_options',
'icon' => 'dashicons-groups',
'position' => 30,
'labels' => [
'singular' => 'customer',
'plural' => 'customers',
],
// ...
] );The legacy page config key is still supported and maps to menu_slug automatically. The get_table_renderer()
function is still available for edge cases where you need to embed a table in an existing page.
- PHP 7.4+
- WordPress 5.0+
- BerlinDB-based custom tables
- arraypress/wp-composer-assets
The following ArrayPress libraries are used for column auto-formatting:
- wp-date-utils — Date and duration formatting
- wp-countries — Country flag and name rendering
- wp-currencies — Currency formatting
- wp-status-badge — Status badge rendering
- wp-rate-format — Rate and percentage formatting
GPL-2.0-or-later
Created by David Sherlock at ArrayPress.