diff --git a/assets/css/admin-tables.css b/assets/css/admin-tables.css new file mode 100644 index 00000000000..bd30ea785c2 --- /dev/null +++ b/assets/css/admin-tables.css @@ -0,0 +1,52 @@ +.column-error_status .dashicons-editor-help { + color: #767676; +} +.column-sources_with_invalid_output .dashicons { + margin-right: 5px; +} +.column-sources_with_invalid_output .dashicons-admin-plugins { + color: #64a2e9; +} +.column-sources_with_invalid_output .dashicons-admin-appearance { + color: #ebb04f; +} +.column-sources_with_invalid_output .dashicons-wordpress-alt { + color: #92b371; +} +.amp-logo-icon { + background-image: url( '../images/amp-logo-icon.svg' ); + background-color: transparent; + background-size: 20px 20px; + height: 20px; + width: 20px; + display: inline-block; +} +.column-error_status .error-status { + line-height: 20px; + display: inline-block; + position: relative; + vertical-align: top; + margin-left: 10px; +} +td.column-found_elements_and_attributes { + color: #c06e60; +} +.column-error_status .dashicons-flag.new { + color: #d98501; +} +.column-error_status .dashicons-yes.new { + color: #ff0000; +} +.column-error_status .dashicons-warning.rejected { + color: #68c6ff; +} +.column-sources_with_invalid_output .source { + margin-bottom: 10px; +} +.column-sources_with_invalid_output .source { + margin-bottom: 10px; + display: block; +} +.wrap .wp-heading-inline + .page-title-action { + margin-left: 1rem; +} diff --git a/assets/css/amp-validation-error-taxonomy.css b/assets/css/amp-validation-error-taxonomy.css index 91192865397..74a3937049b 100644 --- a/assets/css/amp-validation-error-taxonomy.css +++ b/assets/css/amp-validation-error-taxonomy.css @@ -74,17 +74,27 @@ details[open] .details-attributes__summary::after { color: #00a0d2; } +.column-sources_with_invalid_output details[open] .details-attributes__summary { + margin-bottom: 5px; +} +.column-sources_with_invalid_output details > div { + padding-left: 25px; +} + /* Error details toggle button */ -.manage-column.column-details { +.manage-column.column-details, .manage-column.column-sources_with_invalid_output { display: flex; justify-content: space-between; align-items: center; } +.manage-column.column-sources_with_invalid_output .error-details-toggle { + margin: 0; +} .error-details-toggle { display: flex; flex-direction: column; - height: 12px; + height: 14px; margin-right: 10px; padding: 0; background: none; diff --git a/assets/images/amp-logo-icon.svg b/assets/images/amp-logo-icon.svg new file mode 100644 index 00000000000..f6f70c9ba55 --- /dev/null +++ b/assets/images/amp-logo-icon.svg @@ -0,0 +1,10 @@ + + + + + + + diff --git a/assets/images/baseline-error-blue.svg b/assets/images/baseline-error-blue.svg new file mode 100644 index 00000000000..fa6d7953888 --- /dev/null +++ b/assets/images/baseline-error-blue.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/assets/images/baseline-error.svg b/assets/images/baseline-error.svg new file mode 100644 index 00000000000..e6d4620d34d --- /dev/null +++ b/assets/images/baseline-error.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/assets/images/editor-help.svg b/assets/images/editor-help.svg new file mode 100755 index 00000000000..e5cdf524755 --- /dev/null +++ b/assets/images/editor-help.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/src/amp-validation-error-detail-toggle.js b/assets/src/amp-validation-error-detail-toggle.js index 85275c20b06..38fb0282d25 100644 --- a/assets/src/amp-validation-error-detail-toggle.js +++ b/assets/src/amp-validation-error-detail-toggle.js @@ -6,7 +6,7 @@ import domReady from '@wordpress/dom-ready'; /** * Localized data */ -import { btnAriaLabel } from 'amp-validation-i18n'; +import { btnAriaLabel, errorIndexLink, errorIndexAnchor } from 'amp-validation-i18n'; const OPEN_CLASS = 'is-open'; @@ -16,12 +16,19 @@ const OPEN_CLASS = 'is-open'; * table column via backend code. */ function addToggleButtons() { - [ ...document.querySelectorAll( 'th.column-details.manage-column' ) ].forEach( th => { + const addButtons = ( th ) => { const button = document.createElement( 'button' ); button.setAttribute( 'aria-label', btnAriaLabel ); button.setAttribute( 'type', 'button' ); button.setAttribute( 'class', 'error-details-toggle' ); th.appendChild( button ); + }; + + [ ...document.querySelectorAll( 'th.column-details.manage-column' ) ].forEach( th => { + addButtons( th ); + } ); + [ ...document.querySelectorAll( 'th.manage-column.column-sources_with_invalid_output' ) ].forEach( th => { + addButtons( th ); } ); } @@ -31,7 +38,7 @@ function addToggleButtons() { function addToggleListener() { let open = false; - const details = [ ...document.querySelectorAll( '.column-details details' ) ]; + const details = [ ...document.querySelectorAll( '.column-details details, .column-sources_with_invalid_output details' ) ]; const toggleButtons = [ ...document.querySelectorAll( 'button.error-details-toggle' ) ]; const onButtonClick = () => { open = ! open; @@ -54,7 +61,21 @@ function addToggleListener() { } ); } +// @todo This should be harmonized with the approach in PHP via AMP_Validation_Error_Taxonomy::render_link_to_errors_by_url(). +function addViewErrorsByTypeLinkButton() { + if ( 'undefined' === typeof errorIndexAnchor || 'undefined' === typeof errorIndexLink ) { + return; + } + const heading = document.querySelector( '.wp-heading-inline' ); + const link = document.createElement( 'a' ); + link.innerText = errorIndexAnchor; + link.setAttribute( 'href', errorIndexLink ); + link.setAttribute( 'class', 'page-title-action' ); + heading.after( link ); +} + domReady( () => { addToggleButtons(); addToggleListener(); + addViewErrorsByTypeLinkButton(); } ); diff --git a/includes/validation/class-amp-invalid-url-post-type.php b/includes/validation/class-amp-invalid-url-post-type.php index 43e68e0a576..b18d0b9a470 100644 --- a/includes/validation/class-amp-invalid-url-post-type.php +++ b/includes/validation/class-amp-invalid-url-post-type.php @@ -85,13 +85,13 @@ public static function register() { self::POST_TYPE_SLUG, array( 'labels' => array( - 'name' => _x( 'Invalid AMP Pages (URLs)', 'post type general name', 'amp' ), - 'menu_name' => __( 'Invalid Pages', 'amp' ), - 'singular_name' => __( 'Invalid AMP Page (URL)', 'amp' ), - 'not_found' => __( 'No invalid AMP pages found', 'amp' ), - 'not_found_in_trash' => __( 'No forgotten invalid AMP pages', 'amp' ), - 'search_items' => __( 'Search invalid AMP pages', 'amp' ), - 'edit_item' => __( 'Invalid AMP Page (URL)', 'amp' ), + 'name' => _x( 'Invalid URLs', 'post type general name', 'amp' ), + 'menu_name' => __( 'Invalid URLs', 'amp' ), + 'singular_name' => __( 'Invalid URL', 'amp' ), + 'not_found' => __( 'No invalid URLs found', 'amp' ), + 'not_found_in_trash' => __( 'No forgotten invalid URLs', 'amp' ), + 'search_items' => __( 'Search invalid URLs', 'amp' ), + 'edit_item' => __( 'Invalid URL', 'amp' ), ), 'supports' => false, 'public' => false, @@ -126,6 +126,8 @@ public static function should_show_in_menu() { * Add admin hooks. */ public static function add_admin_hooks() { + add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_post_list_screen_scripts' ) ); + add_filter( 'dashboard_glance_items', array( __CLASS__, 'filter_dashboard_glance_items' ) ); add_action( 'rightnow_end', array( __CLASS__, 'print_dashboard_glance_styles' ) ); @@ -139,7 +141,6 @@ public static function add_admin_hooks() { add_action( 'restrict_manage_posts', array( __CLASS__, 'render_post_filters' ), 10, 2 ); add_filter( 'manage_' . self::POST_TYPE_SLUG . '_posts_columns', array( __CLASS__, 'add_post_columns' ) ); add_action( 'manage_posts_custom_column', array( __CLASS__, 'output_custom_column' ), 10, 2 ); - add_filter( 'post_row_actions', array( __CLASS__, 'filter_row_actions' ), 10, 2 ); add_filter( 'bulk_actions-edit-' . self::POST_TYPE_SLUG, array( __CLASS__, 'filter_bulk_actions' ), 10, 2 ); add_filter( 'handle_bulk_actions-edit-' . self::POST_TYPE_SLUG, array( __CLASS__, 'handle_bulk_action' ), 10, 3 ); add_action( 'admin_notices', array( __CLASS__, 'print_admin_notice' ) ); @@ -169,6 +170,46 @@ public static function add_admin_hooks() { } ); } + /** + * Enqueue style. + */ + public static function enqueue_post_list_screen_scripts() { + // Styles. + $screen = get_current_screen(); + if ( 'edit-amp_invalid_url' !== $screen->id ) { + return; + } + + wp_enqueue_style( + 'amp-admin-tables', + amp_get_asset_url( 'css/admin-tables.css' ), + false, + AMP__VERSION + ); + wp_enqueue_style( + 'amp-validation-error-taxonomy', + amp_get_asset_url( 'css/amp-validation-error-taxonomy.css' ), + array( 'common' ), + AMP__VERSION + ); + wp_enqueue_script( + 'amp-validation-error-detail-toggle', + amp_get_asset_url( 'js/amp-validation-error-detail-toggle-compiled.js' ), + array(), + AMP__VERSION, + true + ); + wp_localize_script( + 'amp-validation-error-detail-toggle', + 'ampValidationI18n', + array( + 'btnAriaLabel' => esc_attr__( 'Toggle all sources', 'amp' ), + 'errorIndexLink' => get_admin_url( null, 'edit-tags.php?taxonomy=amp_validation_error&post_type=amp_invalid_url' ), + 'errorIndexAnchor' => esc_html__( 'View Error Index', 'amp' ), + ) + ); + } + /** * Add count of how many validation error posts there are to the admin menu. */ @@ -286,25 +327,34 @@ public static function display_invalid_url_validation_error_counts_summary( $pos $result = array(); if ( $counts['new'] ) { - $result[] = esc_html( sprintf( + if ( AMP_Validation_Manager::is_sanitization_forcibly_accepted() ) { + $icon = 'flag'; + } else { + $icon = 'yes'; + } + $result[] = sprintf( /* translators: %s is count */ - __( '❓ New: %s', 'amp' ), + '%2$s: %3$s', + esc_attr( $icon ), + esc_html__( 'New', 'amp' ), number_format_i18n( $counts['new'] ) - ) ); + ); } if ( $counts['accepted'] ) { - $result[] = esc_html( sprintf( - /* translators: %s is count */ - __( '✅ Accepted: %s', 'amp' ), + $result[] = sprintf( + /* translators: 1. Title, 2. %s is count */ + '%1$s: %2$s', + esc_html__( 'Accepted', 'amp' ), number_format_i18n( $counts['accepted'] ) - ) ); + ); } if ( $counts['rejected'] ) { - $result[] = esc_html( sprintf( + $result[] = sprintf( /* translators: %s is count */ - __( '❌ Rejected: %s', 'amp' ), + '%1$s: %2$s', + esc_html__( 'Rejected', 'amp' ), number_format_i18n( $counts['rejected'] ) - ) ); + ); } echo implode( '
', $result ); // WPCS: xss ok. } @@ -544,18 +594,20 @@ public static function add_post_columns( $columns ) { $columns = array_merge( $columns, array( - 'error_status' => esc_html__( 'Error Status', 'amp' ), - AMP_Validation_Error_Taxonomy::REMOVED_ELEMENTS => esc_html__( 'Removed Elements', 'amp' ), - AMP_Validation_Error_Taxonomy::REMOVED_ATTRIBUTES => esc_html__( 'Removed Attributes', 'amp' ), - AMP_Validation_Error_Taxonomy::SOURCES_INVALID_OUTPUT => esc_html__( 'Incompatible Sources', 'amp' ), + AMP_Validation_Error_Taxonomy::ERROR_STATUS => sprintf( '%s', esc_html__( 'Status', 'amp' ) ), // @todo Create actual tooltip. + AMP_Validation_Error_Taxonomy::FOUND_ELEMENTS_AND_ATTRIBUTES => esc_html__( 'Invalid', 'amp' ), + AMP_Validation_Error_Taxonomy::SOURCES_INVALID_OUTPUT => esc_html__( 'Sources', 'amp' ), ) ); + if ( isset( $columns['title'] ) ) { + $columns['title'] = esc_html__( 'URL', 'amp' ); + } + // Move date to end. if ( isset( $columns['date'] ) ) { - $date = $columns['date']; unset( $columns['date'] ); - $columns['date'] = $date; + $columns['date'] = esc_html__( 'Last Checked', 'amp' ); } return $columns; @@ -585,9 +637,9 @@ public static function output_custom_column( $column_name, $post_id ) { } self::display_invalid_url_validation_error_counts_summary( $post_id ); break; - case AMP_Validation_Error_Taxonomy::REMOVED_ELEMENTS: + case AMP_Validation_Error_Taxonomy::FOUND_ELEMENTS_AND_ATTRIBUTES: + $items = array(); if ( ! empty( $error_summary[ AMP_Validation_Error_Taxonomy::REMOVED_ELEMENTS ] ) ) { - $items = array(); foreach ( $error_summary[ AMP_Validation_Error_Taxonomy::REMOVED_ELEMENTS ] as $name => $count ) { if ( 1 === intval( $count ) ) { $items[] = sprintf( '%s', esc_html( $name ) ); @@ -595,82 +647,87 @@ public static function output_custom_column( $column_name, $post_id ) { $items[] = sprintf( '%s (%d)', esc_html( $name ), $count ); } } - echo implode( ', ', $items ); // WPCS: XSS OK. - } else { - esc_html_e( '--', 'amp' ); } - break; - case AMP_Validation_Error_Taxonomy::REMOVED_ATTRIBUTES: if ( ! empty( $error_summary[ AMP_Validation_Error_Taxonomy::REMOVED_ATTRIBUTES ] ) ) { - $items = array(); foreach ( $error_summary[ AMP_Validation_Error_Taxonomy::REMOVED_ATTRIBUTES ] as $name => $count ) { if ( 1 === intval( $count ) ) { - $items[] = sprintf( '%s', esc_html( $name ) ); + $items[] = sprintf( '[%s]', esc_html( $name ) ); } else { - $items[] = sprintf( '%s (%d)', esc_html( $name ), $count ); + $items[] = sprintf( '[%s] (%d)', esc_html( $name ), $count ); } } - echo implode( ', ', $items ); // WPCS: XSS OK. + } + if ( ! empty( $items ) ) { + echo implode( ',
', $items ); // WPCS: XSS OK. } else { esc_html_e( '--', 'amp' ); } break; case AMP_Validation_Error_Taxonomy::SOURCES_INVALID_OUTPUT: if ( isset( $error_summary[ AMP_Validation_Error_Taxonomy::SOURCES_INVALID_OUTPUT ] ) ) { - $sources = array(); - foreach ( $error_summary[ AMP_Validation_Error_Taxonomy::SOURCES_INVALID_OUTPUT ] as $type => $names ) { - foreach ( array_unique( $names ) as $name ) { - $sources[] = sprintf( '%s: %s', esc_html( $type ), esc_html( $name ) ); + $sources = $error_summary[ AMP_Validation_Error_Taxonomy::SOURCES_INVALID_OUTPUT ]; + $output = array(); + + if ( isset( $sources['plugin'] ) ) { + $output[] = '
'; + $plugin_names = array(); + $plugin_slugs = array_unique( $sources['plugin'] ); + $plugins = get_plugins(); + foreach ( $plugin_slugs as $plugin_slug ) { + $name = $plugin_slug; + foreach ( $plugins as $plugin_file => $plugin_data ) { + if ( strtok( $plugin_file, '/' ) === $plugin_slug ) { + $name = $plugin_data['Name']; + break; + } + } + $plugin_names[] = $name; + } + $count = count( $plugin_slugs ); + if ( 1 === $count ) { + $output[] = sprintf( '%s', esc_html__( 'Plugin', 'amp' ) ); + } else { + $output[] = sprintf( '%s (%d)', esc_html__( 'Plugins', 'amp' ), $count ); + } + $output[] = '
'; + $output[] = implode( '
', array_unique( $plugin_names ) ); + $output[] = '
'; + $output[] = '
'; + } + if ( isset( $sources['core'] ) ) { + $output[] = '
'; + $count = count( array_unique( $sources['core'] ) ); + if ( 1 === $count ) { + $output[] = sprintf( '%s', esc_html__( 'Other', 'amp' ) ); + } else { + $output[] = sprintf( '%s (%d)', esc_html__( 'Other', 'amp' ), $count ); + } + $output[] = '
'; + $output[] = implode( '
', array_unique( $sources['core'] ) ); + $output[] = '
'; + $output[] = '
'; + } + if ( isset( $sources['theme'] ) ) { + $output[] = '
'; + $output[] = ''; + $themes = array_unique( $sources['theme'] ); + foreach ( $themes as $theme_slug ) { + $theme_obj = wp_get_theme( $theme_slug ); + if ( ! $theme_obj->errors() ) { + $theme_name = $theme_obj->get( 'Name' ); + } else { + $theme_name = $theme_slug; + } + $output[] = sprintf( '%s
', esc_html( $theme_name ) ); } + $output[] = '
'; } - echo implode( ', ', $sources ); // WPCS: XSS ok. + echo implode( '', $output ); // WPCS: XSS ok. } break; } } - /** - * Adds a 'Recheck' link to the edit.php row actions. - * - * The logic to add the new action is mainly copied from WP_Posts_List_Table::handle_row_actions(). - * - * @param array $actions The actions in the edit.php page. - * @param WP_Post $post The post for the actions. - * @return array $actions The filtered actions. - */ - public static function filter_row_actions( $actions, $post ) { - if ( self::POST_TYPE_SLUG !== $post->post_type ) { - return $actions; - } - - $actions['edit'] = sprintf( - '%s', - esc_url( get_edit_post_link( $post ) ), - esc_html__( 'Details', 'amp' ) - ); - unset( $actions['inline hide-if-no-js'] ); - - $url = self::get_url_from_post( $post ); - if ( $url ) { - $actions['view'] = sprintf( - '%s', - esc_url( add_query_arg( AMP_Validation_Manager::VALIDATE_QUERY_VAR, '', $url ) ), - esc_html__( 'View', 'amp' ) - ); - } - - $actions[ self::VALIDATE_ACTION ] = sprintf( - '%s', - esc_url( self::get_recheck_url( $post ) ), - esc_html__( 'Recheck', 'amp' ) - ); - if ( self::get_post_staleness( $post ) ) { - $actions[ self::VALIDATE_ACTION ] = sprintf( '%s', $actions[ self::VALIDATE_ACTION ] ); - } - - return $actions; - } - /** * Adds a 'Recheck' bulk action to the edit.php page and modifies the 'Move to Trash' text. * @@ -1407,7 +1464,7 @@ public static function print_validation_errors_meta_box( $post ) {
  • - +
  • - + post_type ) { + return $actions; + } + + // Inline edits are not relevant. + unset( $actions['inline hide-if-no-js'] ); + + if ( isset( $actions['edit'] ) ) { + $actions['edit'] = sprintf( + '%s', + esc_url( get_edit_post_link( $post ) ), + esc_html__( 'Details', 'amp' ) + ); + } + + if ( 'trash' !== $post->post_status ) { + $url = self::get_url_from_post( $post ); + if ( $url ) { + $actions['view'] = sprintf( + '%s', + esc_url( add_query_arg( AMP_Validation_Manager::VALIDATE_QUERY_VAR, '', $url ) ), + esc_html__( 'View', 'amp' ) + ); + } + + $actions[ self::VALIDATE_ACTION ] = sprintf( + '%s', + esc_url( self::get_recheck_url( $post ) ), + esc_html__( 'Recheck', 'amp' ) + ); + if ( self::get_post_staleness( $post ) ) { + $actions[ self::VALIDATE_ACTION ] = sprintf( '%s', $actions[ self::VALIDATE_ACTION ] ); + } + } + // Replace 'Trash' text with 'Forget'. if ( isset( $actions['trash'] ) ) { $actions['trash'] = sprintf( @@ -1712,5 +1806,4 @@ public static function filter_bulk_post_updated_messages( $messages, $bulk_count return $messages; } - } diff --git a/includes/validation/class-amp-validation-error-taxonomy.php b/includes/validation/class-amp-validation-error-taxonomy.php index b97c4f4e8ee..8925eaaf6f0 100644 --- a/includes/validation/class-amp-validation-error-taxonomy.php +++ b/includes/validation/class-amp-validation-error-taxonomy.php @@ -157,6 +157,13 @@ class AMP_Validation_Error_Taxonomy { */ const REMOVED_ELEMENTS = 'removed_elements'; + /** + * The key for found elements and attributes. + * + * @var string + */ + const FOUND_ELEMENTS_AND_ATTRIBUTES = 'found_elements_and_attributes'; + /** * The key for removed attributes. * @@ -178,6 +185,13 @@ class AMP_Validation_Error_Taxonomy { */ const REMOVED_SOURCES = 'removed_sources'; + /** + * The key for the error status. + * + * @var string + */ + const ERROR_STATUS = 'error_status'; + /** * Whether the terms_clauses filter should apply to a term query for validation errors to limit to a given status. * @@ -664,7 +678,7 @@ public static function add_admin_hooks() { wp_enqueue_script( 'amp-validation-error-detail-toggle', amp_get_asset_url( 'js/amp-validation-error-detail-toggle-compiled.js' ), - array( 'wp-dom-ready' ), + array(), AMP__VERSION, true ); @@ -887,9 +901,6 @@ public static function render_taxonomy_filters( $taxonomy_name ) { $( function() { // Move the filter UI after the 'Bulk Actions'