';
/**
* Every section the current user may browse: the four builtins plus
* one per eligible custom post type, carrying the same group fields
* WP Explorer computes (`group`, `groupLabel`, `groupIcon`,
* `groupOrder`) so plugin CPTs fold into plugin folders at the root.
*
* @param Os $os Host handle.
* @return array>
*/
function sections( Os $os ) {
$sections = array(
array(
'id' => 'posts',
'label' => __( 'Posts', 'desktop-mode' ),
'icon' => 'dashicons-admin-post',
'kind' => 'post',
'post_type' => 'post',
'capability' => 'edit_posts',
'thumbnails' => true,
// So a row dragged onto the Recycle Bin knows which REST
// collection its trash call hits — same for every section.
'restPath' => 'wp/v2/posts',
),
array(
'id' => 'pages',
'label' => __( 'Pages', 'desktop-mode' ),
'icon' => 'dashicons-admin-page',
'kind' => 'post',
'post_type' => 'page',
'capability' => 'edit_pages',
'thumbnails' => true,
// The list view offers a Parent column to tree-shaped types.
'hierarchical' => true,
'restPath' => 'wp/v2/pages',
),
array(
'id' => 'media',
'label' => __( 'Media', 'desktop-mode' ),
'icon' => 'dashicons-admin-media',
'kind' => 'media',
'post_type' => 'attachment',
'capability' => 'upload_files',
'thumbnails' => true,
),
array(
'id' => 'users',
'label' => __( 'Users', 'desktop-mode' ),
'icon' => 'dashicons-admin-users',
'kind' => 'user',
'post_type' => '',
'capability' => 'list_users',
'thumbnails' => true,
// Whether the toolbar offers Add user — Core's own menu
// gate for user-new.php: create_users, or promote_users on
// multisite, where a site administrator keeps the
// invite-existing half even when the network withholds
// brand-new account creation. The add-user action
// re-checks server-side.
'canAdd' => current_user_can( 'create_users' )
|| ( is_multisite() && current_user_can( 'promote_users' ) ),
),
);
// The WooCommerce sections (Orders, Customers) — ahead of the CPT
// pass, exactly where WP Explorer's entities filter puts them, so
// they lead the Woo folder. Empty unless WooCommerce is active.
$sections = array_merge( $sections, woo_sections( $os ) );
// Every eligible CPT, through the same discovery WP Explorer uses —
// one list of what the site contains, two windows rendering it.
if ( function_exists( 'openstation_my_wordpress_eligible_post_types' ) ) {
$claimed = array_column( $sections, 'post_type' );
foreach ( openstation_my_wordpress_eligible_post_types() as $name => $post_type ) {
// A type an earlier section already declares (Orders claims
// `shop_order`) must not become a second, broken folder.
if ( in_array( (string) $name, $claimed, true ) ) {
continue;
}
$group = function_exists( 'openstation_my_wordpress_post_type_group' )
? openstation_my_wordpress_post_type_group( $name )
: null;
$entry = array(
'id' => 'cpt-' . $name,
'label' => isset( $post_type->labels->name ) && '' !== $post_type->labels->name
? (string) $post_type->labels->name
: (string) $name,
'icon' => function_exists( 'openstation_my_wordpress_post_type_icon' )
? openstation_my_wordpress_post_type_icon( $post_type )
: 'dashicons-admin-post',
'kind' => 'post',
'post_type' => (string) $name,
'capability' => (string) $post_type->cap->edit_posts,
'thumbnails' => post_type_supports( $name, 'thumbnail' ),
'hierarchical' => is_post_type_hierarchical( $name ),
'group' => $group ? (string) $group['id'] : null,
'groupLabel' => $group ? (string) $group['label'] : null,
'groupIcon' => $group ? (string) $group['icon'] : null,
'groupOrder' => $group ? (int) $group['order'] : null,
'restPath' => function_exists( 'openstation_my_wordpress_post_type_rest_path' )
? (string) openstation_my_wordpress_post_type_rest_path( $post_type )
: '',
);
$sections[] = woo_decorate_section( $entry, $post_type );
}
}
// The Agents section, exactly as WP Explorer lists it: always
// discoverable while the user may read agents, even when the
// framework is off — the section itself explains how to turn it on.
if ( function_exists( 'openstation_agents_user_can_read' ) && openstation_agents_user_can_read() ) {
$sections[] = array(
'id' => 'agents',
'label' => __( 'Agents', 'desktop-mode' ),
'icon' => function_exists( 'openstation_agent_avatar_url' )
? openstation_agent_avatar_url()
: 'dashicons-superhero',
'kind' => 'agent',
'post_type' => '',
'capability' => '',
'thumbnails' => false,
);
}
/**
* Filter the sections the My WordPress app offers. Runs on every
* render — a post type registered at any point of the bootstrap
* can appear, unlike a list frozen at registration time.
*
* @param array[] $sections Each: `id`, `label`, `icon`, `kind`
* (`post` | `media` | `user` | `agent`),
* `post_type`, `capability`, `thumbnails`,
* and the optional `group*` folder fields.
*/
$sections = (array) $os->filter( 'openstation_my_wordpress_app_sections', $sections );
return array_values(
array_filter(
$sections,
static function ( $section ) use ( $os ) {
return is_array( $section ) && ! empty( $section['id'] )
&& ( empty( $section['capability'] ) || $os->can( (string) $section['capability'] ) );
}
)
);
}
/**
* One section by id, or null.
*
* @param Os $os Host handle.
* @param string $id Section id.
* @return array|null
*/
function section_of( Os $os, $id ) {
foreach ( sections( $os ) as $section ) {
if ( $section['id'] === $id ) {
return $section;
}
}
return null;
}
/**
* The plugin folders the grouped sections fold into, through the
* same collector (and `openstation_my_wordpress_post_type_groups`
* filter) WP Explorer uses.
*
* @param array[] $sections Section descriptors.
* @return array[] Each: `id`, `label`, `icon`, `order`.
*/
function groups( array $sections ) {
if ( function_exists( 'openstation_my_wordpress_collect_groups' ) ) {
return openstation_my_wordpress_collect_groups( $sections );
}
$groups = array();
foreach ( $sections as $section ) {
if ( ! empty( $section['group'] ) && ! isset( $groups[ $section['group'] ] ) ) {
$groups[ $section['group'] ] = array(
'id' => (string) $section['group'],
'label' => (string) ( $section['groupLabel'] ?? $section['group'] ),
'icon' => (string) ( $section['groupIcon'] ?? 'dashicons-admin-plugins' ),
'order' => (int) ( $section['groupOrder'] ?? 20 ),
);
}
}
return array_values( $groups );
}
/**
* The statuses the explorer lists: everything an editor works on.
*
* @return string[]
*/
function statuses() {
return array( 'publish', 'future', 'draft', 'pending', 'private' );
}
/**
* Sort options for a section's kind: `value => [ label, orderby, order ]`.
*
* @param array $section Section descriptor.
* @return array
*/
function sort_options( array $section ) {
if ( 'agent' === $section['kind'] ) {
return array();
}
$woo = woo_sort_options( $section );
if ( null !== $woo ) {
return $woo;
}
// Every column the list view can sort by is a sort option here, so
// the icon view's "Sort by" menu and the list view's headers are
// two doors onto ONE server order — a column is sortable exactly
// when both of its keys exist in this table.
if ( 'user' === $section['kind'] ) {
return array(
'default' => array( __( 'Name A–Z', 'desktop-mode' ), 'display_name', 'ASC' ),
'title-desc' => array( __( 'Name Z–A', 'desktop-mode' ), 'display_name', 'DESC' ),
'newest' => array( __( 'Recently registered', 'desktop-mode' ), 'registered', 'DESC' ),
'oldest' => array( __( 'Longest registered', 'desktop-mode' ), 'registered', 'ASC' ),
'id-asc' => array( __( 'ID, lowest first', 'desktop-mode' ), 'ID', 'ASC' ),
'id-desc' => array( __( 'ID, highest first', 'desktop-mode' ), 'ID', 'DESC' ),
'login-asc' => array( __( 'Username A–Z', 'desktop-mode' ), 'login', 'ASC' ),
'login-desc' => array( __( 'Username Z–A', 'desktop-mode' ), 'login', 'DESC' ),
'email-asc' => array( __( 'Email A–Z', 'desktop-mode' ), 'email', 'ASC' ),
'email-desc' => array( __( 'Email Z–A', 'desktop-mode' ), 'email', 'DESC' ),
'posts' => array( __( 'Most posts', 'desktop-mode' ), 'post_count', 'DESC' ),
'posts-asc' => array( __( 'Fewest posts', 'desktop-mode' ), 'post_count', 'ASC' ),
);
}
$options = array(
'default' => array( __( 'Newest first', 'desktop-mode' ), 'date', 'DESC' ),
'oldest' => array( __( 'Oldest first', 'desktop-mode' ), 'date', 'ASC' ),
'title-asc' => array( __( 'Title A–Z', 'desktop-mode' ), 'title', 'ASC' ),
'title-desc' => array( __( 'Title Z–A', 'desktop-mode' ), 'title', 'DESC' ),
'id-asc' => array( __( 'ID, lowest first', 'desktop-mode' ), 'ID', 'ASC' ),
'id-desc' => array( __( 'ID, highest first', 'desktop-mode' ), 'ID', 'DESC' ),
'modified' => array( __( 'Recently modified', 'desktop-mode' ), 'modified', 'DESC' ),
'modified-asc' => array( __( 'Least recently modified', 'desktop-mode' ), 'modified', 'ASC' ),
'slug-asc' => array( __( 'Slug A–Z', 'desktop-mode' ), 'name', 'ASC' ),
'slug-desc' => array( __( 'Slug Z–A', 'desktop-mode' ), 'name', 'DESC' ),
);
if ( 'media' !== $section['kind'] ) {
$options['comments'] = array( __( 'Most comments', 'desktop-mode' ), 'comment_count', 'DESC' );
$options['comments-asc'] = array( __( 'Fewest comments', 'desktop-mode' ), 'comment_count', 'ASC' );
}
return $options;
}
/**
* The (orderby, order) pair the state's `sort` resolves to.
*
* @param array $section Section descriptor.
* @param State $state State.
* @return array{0:string,1:string}
*/
function sort_of( array $section, State $state ) {
$options = sort_options( $section );
$picked = (string) $state->get( 'sort' );
$row = $options[ isset( $options[ $picked ] ) ? $picked : 'default' ];
return array( $row[1], $row[2] );
}