diff --git a/assets/src/amp-validation/amp-validated-url-post-edit-screen.js b/assets/src/amp-validation/amp-validated-url-post-edit-screen.js index c2ff74bde4d..8ea1ed16343 100644 --- a/assets/src/amp-validation/amp-validated-url-post-edit-screen.js +++ b/assets/src/amp-validation/amp-validated-url-post-edit-screen.js @@ -8,6 +8,8 @@ import { __, _n, sprintf } from '@wordpress/i18n'; * Internal dependencies */ import setValidationErrorRowsSeenClass from './set-validation-error-rows-seen-class'; +import { handleCopyToClipboardButtons } from './copy-to-clipboard-buttons'; +import { getURLValidationTableRows } from './get-url-validation-table-rows'; /** * The id for the 'Showing x of y errors' notice. @@ -32,6 +34,7 @@ domReady( () => { handleBulkActions(); watchForUnsavedChanges(); setupStylesheetsMetabox(); + handleCopyToClipboardButtons(); } ); let beforeUnloadPromptAdded = false; @@ -389,13 +392,10 @@ const handleBulkActions = () => { const markingAsReviewed = target.classList.contains( 'reviewed' ); - [ ...document.querySelectorAll( 'select.amp-validation-error-status' ) ].forEach( ( select ) => { - const row = select.closest( 'tr' ); - if ( row.querySelector( '.check-column input[type=checkbox]' ).checked ) { - row.querySelector( 'input[type=checkbox].amp-validation-error-status-review' ).checked = markingAsReviewed; - row.classList.toggle( 'new', ! markingAsReviewed ); - addBeforeUnloadPrompt(); - } + getURLValidationTableRows( { checkedOnly: true } ).forEach( ( row ) => { + row.querySelector( 'input[type=checkbox].amp-validation-error-status-review' ).checked = markingAsReviewed; + row.classList.toggle( 'new', ! markingAsReviewed ); + addBeforeUnloadPrompt(); } ); } ); }; diff --git a/assets/src/amp-validation/copy-to-clipboard-buttons.js b/assets/src/amp-validation/copy-to-clipboard-buttons.js new file mode 100644 index 00000000000..d19bbcdbc66 --- /dev/null +++ b/assets/src/amp-validation/copy-to-clipboard-buttons.js @@ -0,0 +1,76 @@ +/** + * External dependencies + */ +import Clipboard from 'clipboard'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { getURLValidationTableRows } from './get-url-validation-table-rows'; + +/** + * Success handler, called when data is copied to the clipboard. + * + * @param {Object} event + * @param {HTMLElement} event.trigger The element triggering the event. + */ +function onSuccess( { trigger } ) { + trigger.focus(); + + const newInnerText = __( 'Copied to clipboard', 'amp' ); + + // Exit if the user has already clicked the button and we are still within the + // 4000ms before the setTimeout callback runs. + if ( trigger.innerText === newInnerText ) { + return; + } + + const originalText = trigger.innerText; + trigger.innerText = newInnerText; + + setTimeout( () => { + if ( document.body.contains( trigger ) ) { + trigger.innerText = originalText; + } + }, 4000 ); +} + +/** + * Sets up the "Copy to clipboard" buttons on the URL validation screen. + */ +export function handleCopyToClipboardButtons() { + const clipboards = []; + + // eslint-disable-next-line no-new + clipboards.push( new Clipboard( 'button.single-url-detail-copy', { + text: ( btn ) => { + return JSON.stringify( JSON.parse( btn.getAttribute( 'data-error-json' ) ), null, '\t' ); + }, + } ) ); + + // eslint-disable-next-line no-new + clipboards.push( new Clipboard( 'button.copy-all', { + text: () => { + const value = getURLValidationTableRows( { checkedOnly: true } ).map( ( row ) => { + const copyButton = row.querySelector( '.single-url-detail-copy' ); + if ( ! copyButton ) { + return null; + } + + return JSON.parse( copyButton.getAttribute( 'data-error-json' ) ); + } ) + .filter( ( item ) => item ); + + return JSON.stringify( value, null, '\t' ); + }, + } ) ); + + clipboards.forEach( ( clipboard ) => { + clipboard.on( 'success', onSuccess ); + } ); +} diff --git a/assets/src/amp-validation/get-url-validation-table-rows.js b/assets/src/amp-validation/get-url-validation-table-rows.js new file mode 100644 index 00000000000..9d654c6f343 --- /dev/null +++ b/assets/src/amp-validation/get-url-validation-table-rows.js @@ -0,0 +1,16 @@ +/** + * Gets the table rows on a single URL validation screen. + * + * @param {Object} options + * @param {boolean} options.checkedOnly Whether to return only checked rows. + */ +export function getURLValidationTableRows( options = {} ) { + const rows = [ ...document.querySelectorAll( 'select.amp-validation-error-status' ) ] + .map( ( select ) => select.closest( 'tr' ) ); + + if ( true !== options.checkedOnly ) { + return rows; + } + + return rows.filter( ( row ) => row.querySelector( '.check-column input[type=checkbox]' ).checked ); +} diff --git a/includes/validation/class-amp-validated-url-post-type.php b/includes/validation/class-amp-validated-url-post-type.php index d98c1ae4665..95d934d1b5a 100644 --- a/includes/validation/class-amp-validated-url-post-type.php +++ b/includes/validation/class-amp-validated-url-post-type.php @@ -2796,6 +2796,7 @@ public static function render_single_url_list_table( $post ) { +
diff --git a/includes/validation/class-amp-validation-error-taxonomy.php b/includes/validation/class-amp-validation-error-taxonomy.php index f4bd1668e0c..b13f531c39f 100644 --- a/includes/validation/class-amp-validation-error-taxonomy.php +++ b/includes/validation/class-amp-validation-error-taxonomy.php @@ -1591,6 +1591,31 @@ public static function add_admin_notices() { } } + /** + * Returns JSON-formatted error details for an error term. + * + * @param WP_Term $term The term. + * @return string Encoded JSON. + */ + public static function get_error_details_json( $term ) { + $json = json_decode( $term->description, true ); + + // Convert the numeric constant value of the node_type to its constant name. + $xml_reader_reflection_class = new ReflectionClass( 'XMLReader' ); + $constants = $xml_reader_reflection_class->getConstants(); + foreach ( $constants as $key => $value ) { + if ( $json['node_type'] === $value ) { + $json['node_type'] = $key; + break; + } + } + + $json['removed'] = (bool) ( (int) $term->term_group & self::ACCEPTED_VALIDATION_ERROR_BIT_MASK ); + $json['reviewed'] = (bool) ( (int) $term->term_group & self::ACKNOWLEDGED_VALIDATION_ERROR_BIT_MASK ); + + return wp_json_encode( $json ); + } + /** * Add row actions. * @@ -1619,6 +1644,12 @@ public static function filter_tag_row_actions( $actions, WP_Term $tag ) { esc_attr__( 'Toggle error details', 'amp' ), esc_html__( 'Details', 'amp' ) ); + + $actions['copy'] = sprintf( + '', + esc_attr( self::get_error_details_json( $term ) ), + esc_html__( 'Copy to clipboard', 'amp' ) + ); } elseif ( 'edit-tags.php' === $pagenow ) { $actions['details'] = sprintf( '%s', @@ -1646,7 +1677,7 @@ public static function filter_tag_row_actions( $actions, WP_Term $tag ) { } } - $actions = wp_array_slice_assoc( $actions, [ 'details', 'delete' ] ); + $actions = wp_array_slice_assoc( $actions, [ 'details', 'delete', 'copy' ] ); return $actions; } @@ -2251,7 +2282,6 @@ public static function render_single_url_error_details( $validation_error, $term - assertTrue( array_key_exists( 'copy', $actions ) ); + $this->assertStringContains( 'Copy to clipboard', $actions['copy'] ); + } /** @@ -1521,4 +1527,44 @@ public function get_mock_error() { 'node_type' => XML_ELEMENT_NODE, ]; } + + /** + * Test get_error_details_json. + * + * @covers \AMP_Validation_Error_Taxonomy::get_error_details_json() + */ + public function test_get_error_details_json() { + $term = new WP_Term( + (object) [ + 'description' => wp_json_encode( + [ + 'node_type' => 1, + ] + ), + 'term_group' => 1, + ] + ); + + $result = json_decode( AMP_Validation_Error_Taxonomy::get_error_details_json( $term ), true ); + + $this->assertEquals( 'ELEMENT', $result['node_type'] ); + $this->assertEquals( true, $result['removed'] ); + $this->assertEquals( false, $result['reviewed'] ); + + $term = new WP_Term( + (object) [ + 'description' => wp_json_encode( + [ + 'node_type' => 2, + ] + ), + 'term_group' => 2, + ] + ); + $result = json_decode( AMP_Validation_Error_Taxonomy::get_error_details_json( $term ), true ); + + $this->assertEquals( 'ATTRIBUTE', $result['node_type'] ); + $this->assertEquals( false, $result['removed'] ); + $this->assertEquals( true, $result['reviewed'] ); + } }