Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add functionality to reset page order #129

Merged
merged 10 commits into from
Feb 28, 2023
16 changes: 16 additions & 0 deletions assets/js/src/simple-page-ordering.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,19 @@ sortable_post_table.sortable({
}
}
});

jQuery( function() {
// set up click handler for order reset link
jQuery( '#simple-page-ordering-reset' ).on( 'click', function(e) {
e.preventDefault();
var post_type = jQuery( this ).data( 'posttype' );
if ( window.confirm( 'Are you sure you want to reset the ' + post_type + ' order?' ) ) {
jQuery.post( ajaxurl, {
action: 'reset_simple_page_ordering',
post_type: post_type,
_wpnonce: simple_page_ordering_localized_data._wpnonce,
screen_id: simple_page_ordering_localized_data.screen_id
}, function() { window.location.reload(); } );
}
} );
});
39 changes: 37 additions & 2 deletions simple-page-ordering.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public function __construct() {}
public static function add_actions() {
add_action( 'load-edit.php', array( __CLASS__, 'load_edit_screen' ) );
add_action( 'wp_ajax_simple_page_ordering', array( __CLASS__, 'ajax_simple_page_ordering' ) );
add_action( 'wp_ajax_reset_simple_page_ordering', array( __CLASS__, 'ajax_reset_simple_page_ordering' ) );
add_action( 'plugins_loaded', array( __CLASS__, 'load_textdomain' ) );
add_action( 'rest_api_init', array( __CLASS__, 'rest_api_init' ) );
}
Expand Down Expand Up @@ -164,12 +165,13 @@ function () {
* Add page ordering help to the help tab
*/
public static function admin_head() {
$screen = get_current_screen();
$reset_order = sprintf( '<a href="#" id="simple-page-ordering-reset" data-posttype="%s">%s</a>', get_query_var( 'post_type' ), __( 'Reset post order', 'simple-page-ordering' ) );
$screen = get_current_screen();
$screen->add_help_tab(
array(
'id' => 'simple_page_ordering_help_tab',
'title' => 'Simple Page Ordering',
'content' => '<p>' . __( 'To reposition an item, simply drag and drop the row by "clicking and holding" it anywhere (outside of the links and form controls) and moving it to its new position.', 'simple-page-ordering' ) . '</p>',
'content' => '<p>' . __( 'To reposition an item, simply drag and drop the row by "clicking and holding" it anywhere (outside of the links and form controls) and moving it to its new position.', 'simple-page-ordering' ) . '</p></p>' . $reset_order . '</p>',
)
);
}
Expand Down Expand Up @@ -219,6 +221,39 @@ public static function ajax_simple_page_ordering() {
die( wp_json_encode( $result ) );
}

/**
* Page ordering reset ajax callback
*
* @return void
*/
public static function ajax_reset_simple_page_ordering() {
global $wpdb;

// check and make sure we have what we need
$post_type = $_POST['post_type'];
if ( empty( $post_type ) ) {
die( -1 );
}

// do we have a nonce that verifies?
if ( empty( $_POST['_wpnonce'] ) || empty( $_POST['screen_id'] ) ) {
// no nonce to verify...
die( -1 );
}

check_admin_referer( 'simple-page-ordering_' . sanitize_key( $_POST['screen_id'] ) );

// does user have the right to manage these post objects?
if ( ! self::check_edit_others_caps( $post_type ) ) {
die( -1 );
}

// reset the order of all posts of given post type
$wpdb->update( 'wp_posts', array( 'menu_order' => 0 ), array( 'post_type' => $post_type ) );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dkotter This PR works well. The only area that I am concerned about is the performance impact due to this line.
Would this be an issue for an extremely large database? If so, should we look into performing the updates in batches?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the code wpdb::update runs, seems like the final query will be something like:

UPDATE 'wp_posts' SET 'menu_order' = 0 WHERE 'post_type' = 'page'

I think that should scale fairly well and we won't need to worry about doing any sort of batching here. That said, @ruscoe if you'd be able to do some quick testing on various sizes to see the performance impact, that would help us be more confident about merging this in. Something like profiling this code when run on a site that has 10 pages, 100 pages and 1000 pages, that would be helpful.

In addition, I think best practice here is to utilize the format properties of the update method. Something like

Suggested change
$wpdb->update( 'wp_posts', array( 'menu_order' => 0 ), array( 'post_type' => $post_type ) );
$wpdb->update( 'wp_posts', array( 'menu_order' => 0 ), array( 'post_type' => $post_type ), array( '%d' ), array( '%s' ) );

Copy link
Contributor Author

@ruscoe ruscoe Feb 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dkotter No problem at all! I profiled by measuring execution time.

Just to show my process, first I used a quick shell script to create pages:

#!/bin/bash

for i in {1..10}
do
  wp post create --post_type=page --post_title="Page $i"
done

Then used the following to test:

$start_time = microtime( true );
$wpdb->update( 'wp_posts', array( 'menu_order' => 0 ), array( 'post_type' => 'page' ), array( '%d' ), array( '%s' ) );
$end_time = microtime( true );
$total_time = ( $end_time - $start_time );
echo $total_time;

Taking the average of five test runs each:

10 posts: 0.0041 microseconds

100 posts: 0.0042 microseconds

1000 posts: 0.0116 microseconds

5000 posts: 0.0299 microseconds

How does that look?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ruscoe! Definitely see an increase in time as the number of posts increase but the overall amount of time is so low that this doesn't seem like it will cause any problems. This looks good to go on my end


die( 0 );
}

/**
* Page ordering function
*
Expand Down
43 changes: 43 additions & 0 deletions tests/cypress/integration/reset-page-ordering.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
describe('Test Reset Page Order Change', () => {
it('Can reset pages order', () => {
cy.login();
cy.visit('/wp-admin/edit.php?post_type=page');

const first = '.wp-list-table tbody tr:nth-child(1)';
const second = '.wp-list-table tbody tr:nth-child(2)';
const firstText = cy.get(`${first} .row-title`);
const secondText = cy.get(`${second} .row-title`);

// first reorder the pages.
cy.get(first).drag(second);
// wait for order update done.
cy.get(`${second} .check-column input`).should('exist');

cy.get(`${first} .row-title`).then($el => {
secondText.should('have.text', $el.text());
});
cy.get(`${second} .row-title`).then($el => {
firstText.should('have.text', $el.text());
});

// now reset the page order and verify original values are back.
cy.get(`#contextual-help-link`).click();
cy.get(`#tab-link-simple_page_ordering_help_tab`).click();
cy.get(`#simple-page-ordering-reset`).click();
cy.on(`window:confirm`, () => true);

// wait for the page to reload before checking for reset post order.
cy.wait(1000);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cypress considers using cy.wait() as anti-pattern. We can replace this with cy.reload() instead because window.location.reload() will not actually reload the page in cypress.


// Re-query row elements that were detached from the DOM during the page reload.
const newFirstText = cy.get(`${first} .row-title`);
const newSecondText = cy.get(`${second} .row-title`);

cy.get(`${first} .row-title`).then($el => {
newFirstText.should('have.text', $el.text());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will always be true, as newFirstText and cy.get('${first} .row-title') refer to the same element.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to save the original post names so that we can verify their positions after swapping and reverting.
This is something I was trying today, I used cy.as() to store the text values and refer them later on for verification:

describe( 'Test Reset Page Order Change', () => {
	it( 'Can reset pages order', () => {
		cy.login();
		cy.visit('/wp-admin/edit.php?post_type=page');

		const firstRow = '.wp-list-table tbody tr:nth-child(1)';
		const secondRow = '.wp-list-table tbody tr:nth-child(2)';

		// Alias titles as `firstRowText` and `secondRowText` for convenience.
		cy.get( firstRow ).find( '.row-title' ).invoke( 'text' ).as( 'firstRowText' );
		cy.get( secondRow ).find( '.row-title' ).invoke( 'text' ).as( 'secondRowText' );

		// Swap position of `Page 1` with `Page 2`.
		cy.get( firstRow ).drag( secondRow );

		// Verifies if 1st row has title `Page 2`.
		cy.get( firstRow ).find( '.row-title' ).invoke( 'text' ).then( function( text ) {
			expect( text ).to.eq( this.secondRowText );
		} );

		// Verifies if 2nd row has title `Page 1`.
		cy.get( secondRow ).find( '.row-title' ).invoke( 'text' ).then( function( text ) {
			expect( text ).to.eq( this.firstRowText );
		} );

		// Now reset the page order and verify original values are back.
		cy.get( '#contextual-help-link' ).click();
		cy.get( '#tab-link-simple_page_ordering_help_tab' ).click();
		cy.get( '#simple-page-ordering-reset' ).click();
		cy.on( 'window:confirm', () => true );

		// Perform a reload as Cypress won't after window:confirm.
		cy.reload();

		// Verifies if 1st row has title `Page 1`.
		cy.get( firstRow ).find( '.row-title' ).invoke( 'text' ).then( function( text ) {
			expect( text ).to.eq( this.firstRowText );
		} );

		// Verifies if 2nd row has title `Page 2`.
		cy.get( secondRow ).find( '.row-title' ).invoke( 'text' ).then( function( text ) {
			expect( text ).to.eq( this.secondRowText );
		} );
	} );
} );

});
cy.get(`${second} .row-title`).then($el => {
newSecondText.should('have.text', $el.text());
});
});
});