Skip to content

Commit 1160864

Browse files
authored
Merge pull request #5500 from ampproject/feature/5209-clipboard-friendly-errors
Add buttons to copy error details to clipboard
2 parents 125d06d + f6bb8ad commit 1160864

File tree

6 files changed

+178
-9
lines changed

6 files changed

+178
-9
lines changed

assets/src/amp-validation/amp-validated-url-post-edit-screen.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { __, _n, sprintf } from '@wordpress/i18n';
88
* Internal dependencies
99
*/
1010
import setValidationErrorRowsSeenClass from './set-validation-error-rows-seen-class';
11+
import { handleCopyToClipboardButtons } from './copy-to-clipboard-buttons';
12+
import { getURLValidationTableRows } from './get-url-validation-table-rows';
1113

1214
/**
1315
* The id for the 'Showing x of y errors' notice.
@@ -32,6 +34,7 @@ domReady( () => {
3234
handleBulkActions();
3335
watchForUnsavedChanges();
3436
setupStylesheetsMetabox();
37+
handleCopyToClipboardButtons();
3538
} );
3639

3740
let beforeUnloadPromptAdded = false;
@@ -389,13 +392,10 @@ const handleBulkActions = () => {
389392

390393
const markingAsReviewed = target.classList.contains( 'reviewed' );
391394

392-
[ ...document.querySelectorAll( 'select.amp-validation-error-status' ) ].forEach( ( select ) => {
393-
const row = select.closest( 'tr' );
394-
if ( row.querySelector( '.check-column input[type=checkbox]' ).checked ) {
395-
row.querySelector( 'input[type=checkbox].amp-validation-error-status-review' ).checked = markingAsReviewed;
396-
row.classList.toggle( 'new', ! markingAsReviewed );
397-
addBeforeUnloadPrompt();
398-
}
395+
getURLValidationTableRows( { checkedOnly: true } ).forEach( ( row ) => {
396+
row.querySelector( 'input[type=checkbox].amp-validation-error-status-review' ).checked = markingAsReviewed;
397+
row.classList.toggle( 'new', ! markingAsReviewed );
398+
addBeforeUnloadPrompt();
399399
} );
400400
} );
401401
};
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import Clipboard from 'clipboard';
5+
6+
/**
7+
* WordPress dependencies
8+
*/
9+
import { __ } from '@wordpress/i18n';
10+
11+
/**
12+
* Internal dependencies
13+
*/
14+
import { getURLValidationTableRows } from './get-url-validation-table-rows';
15+
16+
/**
17+
* Success handler, called when data is copied to the clipboard.
18+
*
19+
* @param {Object} event
20+
* @param {HTMLElement} event.trigger The element triggering the event.
21+
*/
22+
function onSuccess( { trigger } ) {
23+
trigger.focus();
24+
25+
const newInnerText = __( 'Copied to clipboard', 'amp' );
26+
27+
// Exit if the user has already clicked the button and we are still within the
28+
// 4000ms before the setTimeout callback runs.
29+
if ( trigger.innerText === newInnerText ) {
30+
return;
31+
}
32+
33+
const originalText = trigger.innerText;
34+
trigger.innerText = newInnerText;
35+
36+
setTimeout( () => {
37+
if ( document.body.contains( trigger ) ) {
38+
trigger.innerText = originalText;
39+
}
40+
}, 4000 );
41+
}
42+
43+
/**
44+
* Sets up the "Copy to clipboard" buttons on the URL validation screen.
45+
*/
46+
export function handleCopyToClipboardButtons() {
47+
const clipboards = [];
48+
49+
// eslint-disable-next-line no-new
50+
clipboards.push( new Clipboard( 'button.single-url-detail-copy', {
51+
text: ( btn ) => {
52+
return JSON.stringify( JSON.parse( btn.getAttribute( 'data-error-json' ) ), null, '\t' );
53+
},
54+
} ) );
55+
56+
// eslint-disable-next-line no-new
57+
clipboards.push( new Clipboard( 'button.copy-all', {
58+
text: () => {
59+
const value = getURLValidationTableRows( { checkedOnly: true } ).map( ( row ) => {
60+
const copyButton = row.querySelector( '.single-url-detail-copy' );
61+
if ( ! copyButton ) {
62+
return null;
63+
}
64+
65+
return JSON.parse( copyButton.getAttribute( 'data-error-json' ) );
66+
} )
67+
.filter( ( item ) => item );
68+
69+
return JSON.stringify( value, null, '\t' );
70+
},
71+
} ) );
72+
73+
clipboards.forEach( ( clipboard ) => {
74+
clipboard.on( 'success', onSuccess );
75+
} );
76+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Gets the table rows on a single URL validation screen.
3+
*
4+
* @param {Object} options
5+
* @param {boolean} options.checkedOnly Whether to return only checked rows.
6+
*/
7+
export function getURLValidationTableRows( options = {} ) {
8+
const rows = [ ...document.querySelectorAll( 'select.amp-validation-error-status' ) ]
9+
.map( ( select ) => select.closest( 'tr' ) );
10+
11+
if ( true !== options.checkedOnly ) {
12+
return rows;
13+
}
14+
15+
return rows.filter( ( row ) => row.querySelector( '.check-column input[type=checkbox]' ).checked );
16+
}

includes/validation/class-amp-validated-url-post-type.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2796,6 +2796,7 @@ public static function render_single_url_list_table( $post ) {
27962796
<button type="button" class="button action keep"><?php esc_html_e( 'Keep', 'amp' ); ?></button>
27972797
<button type="button" class="button action reviewed-toggle reviewed"><?php esc_html_e( 'Mark reviewed', 'amp' ); ?></button>
27982798
<button type="button" class="button action reviewed-toggle unreviewed"><?php esc_html_e( 'Mark unreviewed', 'amp' ); ?></button>
2799+
<button type="button" class="button action copy-all"><?php esc_html_e( 'Copy to clipboard', 'amp' ); ?></button>
27992800
<div id="vertical-divider"></div>
28002801
</div>
28012802
<div id="url-post-filter" class="alignleft actions">

includes/validation/class-amp-validation-error-taxonomy.php

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1591,6 +1591,31 @@ public static function add_admin_notices() {
15911591
}
15921592
}
15931593

1594+
/**
1595+
* Returns JSON-formatted error details for an error term.
1596+
*
1597+
* @param WP_Term $term The term.
1598+
* @return string Encoded JSON.
1599+
*/
1600+
public static function get_error_details_json( $term ) {
1601+
$json = json_decode( $term->description, true );
1602+
1603+
// Convert the numeric constant value of the node_type to its constant name.
1604+
$xml_reader_reflection_class = new ReflectionClass( 'XMLReader' );
1605+
$constants = $xml_reader_reflection_class->getConstants();
1606+
foreach ( $constants as $key => $value ) {
1607+
if ( $json['node_type'] === $value ) {
1608+
$json['node_type'] = $key;
1609+
break;
1610+
}
1611+
}
1612+
1613+
$json['removed'] = (bool) ( (int) $term->term_group & self::ACCEPTED_VALIDATION_ERROR_BIT_MASK );
1614+
$json['reviewed'] = (bool) ( (int) $term->term_group & self::ACKNOWLEDGED_VALIDATION_ERROR_BIT_MASK );
1615+
1616+
return wp_json_encode( $json );
1617+
}
1618+
15941619
/**
15951620
* Add row actions.
15961621
*
@@ -1619,6 +1644,12 @@ public static function filter_tag_row_actions( $actions, WP_Term $tag ) {
16191644
esc_attr__( 'Toggle error details', 'amp' ),
16201645
esc_html__( 'Details', 'amp' )
16211646
);
1647+
1648+
$actions['copy'] = sprintf(
1649+
'<button type="button" class="single-url-detail-copy button-link" data-error-json="%s">%s</button>',
1650+
esc_attr( self::get_error_details_json( $term ) ),
1651+
esc_html__( 'Copy to clipboard', 'amp' )
1652+
);
16221653
} elseif ( 'edit-tags.php' === $pagenow ) {
16231654
$actions['details'] = sprintf(
16241655
'<a href="%s">%s</a>',
@@ -1646,7 +1677,7 @@ public static function filter_tag_row_actions( $actions, WP_Term $tag ) {
16461677
}
16471678
}
16481679

1649-
$actions = wp_array_slice_assoc( $actions, [ 'details', 'delete' ] );
1680+
$actions = wp_array_slice_assoc( $actions, [ 'details', 'delete', 'copy' ] );
16501681

16511682
return $actions;
16521683
}
@@ -2251,7 +2282,6 @@ public static function render_single_url_error_details( $validation_error, $term
22512282
</dd>
22522283
<?php endforeach; ?>
22532284
</dl>
2254-
22552285
<?php
22562286

22572287
$output = ob_get_clean();

tests/php/validation/test-class-amp-validation-error-taxonomy.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -986,6 +986,12 @@ function ( $actions ) {
986986
[ 'details', 'delete' ],
987987
array_keys( $filtered_actions )
988988
);
989+
990+
$pagenow = 'post.php';
991+
$actions = AMP_Validation_Error_Taxonomy::filter_tag_row_actions( $initial_actions, get_term( $term_this_taxonomy ) );
992+
$this->assertTrue( array_key_exists( 'copy', $actions ) );
993+
$this->assertStringContains( 'Copy to clipboard', $actions['copy'] );
994+
989995
}
990996

991997
/**
@@ -1521,4 +1527,44 @@ public function get_mock_error() {
15211527
'node_type' => XML_ELEMENT_NODE,
15221528
];
15231529
}
1530+
1531+
/**
1532+
* Test get_error_details_json.
1533+
*
1534+
* @covers \AMP_Validation_Error_Taxonomy::get_error_details_json()
1535+
*/
1536+
public function test_get_error_details_json() {
1537+
$term = new WP_Term(
1538+
(object) [
1539+
'description' => wp_json_encode(
1540+
[
1541+
'node_type' => 1,
1542+
]
1543+
),
1544+
'term_group' => 1,
1545+
]
1546+
);
1547+
1548+
$result = json_decode( AMP_Validation_Error_Taxonomy::get_error_details_json( $term ), true );
1549+
1550+
$this->assertEquals( 'ELEMENT', $result['node_type'] );
1551+
$this->assertEquals( true, $result['removed'] );
1552+
$this->assertEquals( false, $result['reviewed'] );
1553+
1554+
$term = new WP_Term(
1555+
(object) [
1556+
'description' => wp_json_encode(
1557+
[
1558+
'node_type' => 2,
1559+
]
1560+
),
1561+
'term_group' => 2,
1562+
]
1563+
);
1564+
$result = json_decode( AMP_Validation_Error_Taxonomy::get_error_details_json( $term ), true );
1565+
1566+
$this->assertEquals( 'ATTRIBUTE', $result['node_type'] );
1567+
$this->assertEquals( false, $result['removed'] );
1568+
$this->assertEquals( true, $result['reviewed'] );
1569+
}
15241570
}

0 commit comments

Comments
 (0)