templates = $templates; add_action( 'admin_menu', array( $this, 'menu' ), 5 ); add_action( 'admin_menu', array( $this, 'reorder_menu' ), 99 ); add_action( 'admin_enqueue_scripts', array( $this, 'assets' ) ); add_filter( 'manage_shapeoptions_group_posts_columns', array( $this, 'columns' ) ); add_action( 'manage_shapeoptions_group_posts_custom_column', array( $this, 'column_content' ), 10, 2 ); add_action( 'save_post_shapeoptions_group', array( __CLASS__, 'flush_stats' ) ); add_action( 'deleted_post', array( __CLASS__, 'flush_stats_for_post' ), 10, 2 ); add_action( 'trashed_post', array( __CLASS__, 'flush_stats_for_post' ) ); add_action( 'untrashed_post', array( __CLASS__, 'flush_stats_for_post' ) ); } /** * Drop the cached overview figures. */ public static function flush_stats() { delete_transient( self::STATS_TRANSIENT ); } /** * Drop them only for option groups — these hooks fire for every post type. * * @param int $post_id Post ID. * @param WP_Post|null $post Post object, when the action supplies one. */ public static function flush_stats_for_post( $post_id, $post = null ) { $post_type = $post instanceof WP_Post ? $post->post_type : get_post_type( $post_id ); if ( 'shapeoptions_group' === $post_type ) { self::flush_stats(); } } /** * Register the dashboard page. */ public function menu() { add_submenu_page( 'edit.php?post_type=shapeoptions_group', __( 'ShapeOptions Dashboard', 'shapeoptions' ), __( 'Dashboard', 'shapeoptions' ), 'manage_woocommerce', 'shapeoptions-dashboard', array( $this, 'render_page' ) ); } /** * Put submenu items in a deliberate order: Dashboard first, then * groups, then tools. Unknown (add-on) slugs keep their * relative order after the known ones they follow. */ public function reorder_menu() { global $submenu; $parent = 'edit.php?post_type=shapeoptions_group'; if ( empty( $submenu[ $parent ] ) ) { return; } $order = array( 'shapeoptions-dashboard', 'edit.php?post_type=shapeoptions_group', 'post-new.php?post_type=shapeoptions_group', 'shapeoptions-templates', 'shapeoptions-style-settings', 'shapeoptions-import-export', ); usort( $submenu[ $parent ], function ( $a, $b ) use ( $order ) { $pa = array_search( $a[2], $order, true ); $pb = array_search( $b[2], $order, true ); $pa = ( false === $pa ) ? 50 : $pa; $pb = ( false === $pb ) ? 50 : $pb; return $pa <=> $pb; } ); // "Option Groups" reads better as "All Groups" next to Dashboard. foreach ( $submenu[ $parent ] as &$item ) { if ( 'edit.php?post_type=shapeoptions_group' === $item[2] ) { $item[0] = __( 'All Groups', 'shapeoptions' ); } } unset( $item ); } /** * Load the dashboard CSS where needed. * * @param string $hook Current admin page hook suffix. */ public function assets( $hook ) { $screen = get_current_screen(); $is_dash = 'shapeoptions_group_page_shapeoptions-dashboard' === $hook; $is_list = $screen && 'edit-shapeoptions_group' === $screen->id; if ( ! $is_dash && ! $is_list ) { return; } wp_enqueue_style( 'shapeoptions-admin-dashboard', SHAPEOPTIONS_URL . 'assets/css/shapeoptions-admin-dashboard.css', array(), shapeoptions_asset_version( SHAPEOPTIONS_PATH . 'assets/css/shapeoptions-admin-dashboard.css', SHAPEOPTIONS_VERSION ) ); } /* --------------------------------------------------------------- * List table columns. * ------------------------------------------------------------- */ /** * Replace the bare columns with an informative set. * * @param array $columns Default columns. * @return array */ public function columns( $columns ) { return array( 'cb' => $columns['cb'], 'title' => __( 'Group', 'shapeoptions' ), 'shapeoptions_status' => __( 'Status', 'shapeoptions' ), 'shapeoptions_fields' => __( 'Fields', 'shapeoptions' ), 'shapeoptions_applies' => __( 'Shows on', 'shapeoptions' ), 'shapeoptions_placement' => __( 'Placement', 'shapeoptions' ), 'date' => __( 'Date', 'shapeoptions' ), ); } /** * Translated label for a group's post status. * * Shared by the list table and the dashboard's recent-groups table, which * used to carry two different versions of this — and the second one fell * back to `ucfirst( $status )`, i.e. it printed the raw English status * slug. On a translated admin that is the one word on the screen that * stays in English. * * Anything outside ShapeOptions's own four wordings falls back to the * status object's label, which WordPress has already translated, and only * then to the slug. * * @param string $status Post status slug. * @return string */ public static function status_label( $status ) { $labels = array( 'publish' => __( 'Live', 'shapeoptions' ), 'draft' => __( 'Draft', 'shapeoptions' ), 'pending' => __( 'Pending', 'shapeoptions' ), 'trash' => __( 'Trash', 'shapeoptions' ), ); if ( isset( $labels[ $status ] ) ) { return $labels[ $status ]; } $object = get_post_status_object( $status ); return ( $object && ! empty( $object->label ) ) ? $object->label : ucfirst( $status ); } /** * Render our custom column cells. * * @param string $column Column key. * @param int $post_id Group ID. */ public function column_content( $column, $post_id ) { switch ( $column ) { case 'shapeoptions_status': $status = get_post_status( $post_id ); printf( '', esc_attr( $status ), esc_html( self::status_label( $status ) ) ); break; case 'shapeoptions_fields': $fields = get_post_meta( $post_id, '_shapeoptions_fields', true ); $fields = is_array( $fields ) ? $fields : array(); $types = array(); foreach ( $fields as $field ) { if ( ! empty( $field['type'] ) ) { $types[ $field['type'] ] = true; } } $types = array_keys( $types ); $plugin = shapeoptions(); $type_labels = $plugin ? $plugin->fields->get_labels() : array(); printf( '%d ', count( $fields ) ); foreach ( array_slice( $types, 0, 4 ) as $type ) { printf( '', esc_html( $type_labels[ $type ] ?? $type ) ); } if ( count( $types ) > 4 ) { printf( '', count( $types ) - 4 ); } break; case 'shapeoptions_applies': echo esc_html( $this->applies_summary( $post_id ) ); break; case 'shapeoptions_placement': $placement = get_post_meta( $post_id, '_shapeoptions_placement', true ) ?: 'before'; echo esc_html( 'after' === $placement ? __( 'After Add to Cart', 'shapeoptions' ) : __( 'Before Add to Cart', 'shapeoptions' ) ); break; } } /** * Human summary of a group's "Show on" conditions. * * @param int $post_id Group ID. * @return string */ private function applies_summary( $post_id ) { $conditions = get_post_meta( $post_id, '_shapeoptions_conditions', true ); $type = is_array( $conditions ) && ! empty( $conditions['type'] ) ? $conditions['type'] : 'all'; $ids = is_array( $conditions ) && ! empty( $conditions['ids'] ) ? (array) $conditions['ids'] : array(); $count = count( $ids ); switch ( $type ) { case 'products': /* translators: %d: number of products. */ return sprintf( _n( '%d product', '%d products', $count, 'shapeoptions' ), $count ); case 'categories': /* translators: %d: number of categories. */ return sprintf( _n( '%d category', '%d categories', $count, 'shapeoptions' ), $count ); case 'tags': /* translators: %d: number of tags. */ return sprintf( _n( '%d tag', '%d tags', $count, 'shapeoptions' ), $count ); default: return __( 'All products', 'shapeoptions' ); } } /* --------------------------------------------------------------- * Dashboard page. * ------------------------------------------------------------- */ /** * Gather everything the overview shows. Public so it is testable. * * @return array */ public function get_stats() { $cached = get_transient( self::STATS_TRANSIENT ); if ( is_array( $cached ) ) { $cached['templates'] = count( $this->templates->get_templates() ); return $cached; } $counts = wp_count_posts( 'shapeoptions_group' ); $stats = array( 'published' => (int) $counts->publish, 'drafts' => (int) $counts->draft, 'fields' => 0, 'priced' => 0, 'type_usage' => array(), 'templates' => count( $this->templates->get_templates() ), ); $batch = 200; $offset = 0; do { $group_ids = get_posts( array( 'post_type' => 'shapeoptions_group', 'post_status' => array( 'publish', 'draft', 'pending' ), 'posts_per_page' => $batch, 'offset' => $offset, 'orderby' => 'ID', 'order' => 'ASC', 'fields' => 'ids', 'no_found_rows' => true, 'update_post_term_cache' => false, ) ); if ( $group_ids ) { // All of the batch's meta in one query, rather than one per group. update_postmeta_cache( $group_ids ); } foreach ( $group_ids as $group_id ) { $fields = get_post_meta( $group_id, '_shapeoptions_fields', true ); if ( ! is_array( $fields ) ) { continue; } foreach ( $fields as $field ) { $stats['fields']++; $type = ! empty( $field['type'] ) ? $field['type'] : 'text'; if ( ! isset( $stats['type_usage'][ $type ] ) ) { $stats['type_usage'][ $type ] = 0; } $stats['type_usage'][ $type ]++; if ( ! empty( $field['price'] ) ) { $stats['priced']++; } else { foreach ( (array) ( $field['choices'] ?? array() ) as $choice ) { if ( ! empty( $choice['price'] ) ) { $stats['priced']++; break; } } } } } $offset += $batch; } while ( count( $group_ids ) === $batch ); arsort( $stats['type_usage'] ); set_transient( self::STATS_TRANSIENT, $stats, DAY_IN_SECONDS ); return $stats; } /** * Recent groups for the overview table. * * @return WP_Post[] */ private function recent_groups() { return get_posts( array( 'post_type' => 'shapeoptions_group', 'post_status' => array( 'publish', 'draft', 'pending' ), 'posts_per_page' => 6, 'orderby' => 'modified', 'order' => 'DESC', 'no_found_rows' => true, 'update_post_term_cache' => false, ) ); } /** * Render the dashboard. */ public function render_page() { $stats = $this->get_stats(); $groups = $this->recent_groups(); $urls = array( 'new' => admin_url( 'post-new.php?post_type=shapeoptions_group' ), 'all' => admin_url( 'edit.php?post_type=shapeoptions_group' ), 'templates' => admin_url( 'edit.php?post_type=shapeoptions_group&page=shapeoptions-templates' ), 'impexp' => admin_url( 'edit.php?post_type=shapeoptions_group&page=shapeoptions-import-export' ), ); ?>