'; /** * 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] ); }