Skip to content
This repository was archived by the owner on May 21, 2025. It is now read-only.

Pagination Controls for Frontend Subscriptions View #833

Closed

Conversation

barryhughes
Copy link
Member

@barryhughes barryhughes commented Apr 10, 2025

Subscribers can visit My Account ‣ Subscriptions ‣ View Subscription and, amongst other things, they will be presented with a list of related orders. However, there is currently no limit on how many orders are listed. Imagine a subscription that renews daily: if this runs for a few years, we'll have hundreds (and eventually thousands) of related orders.

  • This presents a usability issue:
    • A massive table of hundreds (/thousands) of rows is not easy to consume.
    • It pushes other information out of sight (by default, billing and shipping addresses).
  • It presents a performance issue:
    • Our default caching strategy (see WCS_Related_Order_Store_Cached_CPT) seems to scale really quite well, by storing several lists of related order IDs for each subscription, and I have not touched this. If we do feel a need to adapt this, or supplement the current caching strategy with something else, it should probably be a project of its own.
    • The cost of fetching the raw IDs, then, isn't part of what we address here. Hydration of the order objects is.

With that context in mind, in this PR we introduce a new method to the WC_Subscription class, making it possible to fetch individual pages of related orders, and we add pagination controls to the frontend related orders list.

Fixes WOOSUBS-162 | https://github.com/woocommerce/woocommerce-subscriptions/issues/4751

Further notes

From a local test using a subscription with 339 related orders, I observed the following results (measured using Query Monitor):

Branch Object cache Peak memory (MB) Total DB queries
Trunk off 198 5381
Trunk on (+primed) 147 3241
PR off 131 3124
PR on (+primed) 85 1159

💡 Be aware that if you test with enough orders, and with Query Monitor activated, you may run out of memory. So be ready to adjust that php.ini file and bump memory_limit accordingly (or else reduce the size of the test data set).

How can this code break?

Before this change, we passed an array of all related order IDs into the related-orders.php template whereas, with this change, a maximum of 10 IDs will be passed in. It's possible that, in some cases, our users will have implemented their own pagination (or have some other custom feature that depends on having access to the full range of IDs for some other reason).

I suspect this would be the exception rather than the rule, and if it surfaces we could initially guide them toward restoring the previous behaviour: for example, they could unhook WC_Subscriptions_Order::get_related_orders_template() from the woocommerce_subscription_details_after_subscription_table action, and replace it with a custom callback that follows the earlier strategy.

Additionally, it was previously the case that the woocommerce_subscription_related_orders filter hook would be invoked one or more times (once for each related order type, ie 'renewal', 'switch', etc) whereas with this change, that will no longer happen. Once again, this could cause issues in some cases, and the mitigation would be similar to what I described earlier.

How to test this PR

If you want to look at performance characteristics, I'd recommend using Query Monitor. However, if, like me, you test using a 'real' customer account instead of the main admin account, Query Monitor output won't be available. You can fix this with a small snippet like this:

/**
 * Make Query Monitor available to all users.
 * 
 * ⚠️ Remove after testing!
 */
add_action( 'init', function() {
	$user = wp_get_current_user();

	if ( is_object( $user ) ) {
		$user->add_cap( 'view_query_monitor' );
	}
} );

To functionally test pagination, you may also wish to generate a large-ish number of renewal orders. You can do this manually, via the subscription editor, by repeatedly running the create pending renewal order action. That could get a little tiresome, though, depending on how many orders you want to generate and so, though not perfect, you could also consider leveraging a bit of code like this:

/**
 * This script can be used to quickly add a stack of renewals
 * to an existing subscription. Can be useful when testing
 * scalability concerns for WooCommerce Subscriptions.
 */
function generate_renewal_order_for_subscription( $subscription_id ) {
	$subscription = new WC_Subscription( $subscription_id );

	// We can't successfully trigger creation of a renewal unless the subscription
	// is active, which may not initially be the case.
	if ( 'active' !== $subscription->get_status() ) {
		$subscription->update_status( 'active' );
	}

	// Try triggering a renewal. This also often sets the subscription to on-hold,
	// so we again change the status.
	WC_Subscriptions_Manager::prepare_renewal( $subscription_id );
	$subscription->update_status( 'active' );
}

// Generate 100 renewals for the subscription (adjust the sub ID as needed).
for ( $i = 1; $i <= 100; $i++ ) {
	generate_renewal_order_for_subscription( 12345 ); // ⚠️ Supply actual subscription ID.
	print '.' . ( ( $i % 10 ) ? '' : PHP_EOL );       // Optional CLI progress dots :-)
}

For the testing itself, navigate to a suitable subscription on the frontend: My Account Subscriptions ‣ View Subscription:

  1. A maximum of 10 related orders will be rendered.
  2. "First", "Previous", "Next" and "Last" pagination links will appear below the table.
  3. If they are not useful (for instance, "First" or "Previous" are not useful if we are looking at page 1 of the results) then they still appear, and are still functional, but are styled a little differently by applying the disabled class.
  4. No ajax magic here. However, the links take advantage of an anchor/ID to keep the customer in broadly the same part of the screen whenever they use the pagination controls.

Product impact

  • Added changelog entry (or does not apply)
  • Will this PR affect WooCommerce Subscriptions? Yes → WOOSUBS-162
  • Will this PR affect WooCommerce Payments? No
  • Added deprecated functions, hooks or classes to the spreadsheet *None

@barryhughes
Copy link
Member Author

✍🏼 I referenced 7.4.0 a few times (for @since tags, etc). I am happy to update those as needed, as the timing of this PR means it may not land in that release.

@barryhughes barryhughes requested review from a team and a-danae and removed request for a team April 10, 2025 17:51
@barryhughes
Copy link
Member Author

Lint fails flagged in CI aren't actually related to lines touched in this PR. Happy to address them if preferred (my usual default is not to do that, but, I'm flexible!).

FILE: /home/runner/work/woocommerce-subscriptions-core/woocommerce-subscriptions-core/includes/class-wc-subscriptions-order.php
+------+--------------------------------------------------+
| Line | Description                                      |
+------+--------------------------------------------------+
| 2432 | Processing form data without nonce verification. |
| 2432 | Processing form data without nonce verification. |
+------+--------------------------------------------------+

@barryhughes
Copy link
Member Author

barryhughes commented Apr 10, 2025

✍🏼 One other note: the pagination controls seem (to my easily pleased eye) pretty good when using the usual Twenty Twenty-something themes. However, they don't look quite as pleasing when using Storefront ... but since Storefront is also less of an active concern these days, I'm not sure if we ought to add special CSS rules for it or not (I assume not, but happy to revise as needed).

@barryhughes barryhughes changed the title Fix/162 my subscriptions related orders pagination Pagination Controls for Frontend Subscriptions View Apr 10, 2025
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$page = (int) ( $_GET['related_orders_page'] ?? 1 );
$limit = (int) ( $_GET['related_orders_limit'] ?? 10 );
Copy link
Member Author

Choose a reason for hiding this comment

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

I left this in place ($limit = ...), but happy to remove it. It provides some flexibility for themers/developers, and the attendant risk of a user supplying -1 probably isn't that bad when you consider that is effectively the same as the extant behaviour in our stable release.

An alternative could be introducing a filter hook but, rather than inflate the public API surface pre-emptively, it may be better to wait and see if there is demand for such a thing.

@barryhughes barryhughes force-pushed the fix/162-my-subscriptions-related-orders-pagination branch from bd7b35e to f31f8a0 Compare April 14, 2025 16:49
Copy link

@vbelolapotkov vbelolapotkov left a comment

Choose a reason for hiding this comment

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

I've tested the code and everything works as expected, great enhancement @barryhughes! 🙌

I'm surprised the number of DB queries is still high, but I suppose it's because we still need to fetch meta keys for order to select only related ones.

Anyway, DB query time is reduced ~30% on my local site, so I think it's a good enhancement.

I've made some minor comments, but those are not critical IMO, so approving.

Finally, you've raised some good points in description, but I don't have enough knowledge at the moment to comment on them, so worth asking for additional opinion.

Comment on lines 111 to 114
$first = '?related_orders_page=1#woocommerce-subscriptions-related-orders-table';
$prev = '?related_orders_page=' . $prev_page . '#woocommerce-subscriptions-related-orders-table';
$next = '?related_orders_page=' . $next_page . '#woocommerce-subscriptions-related-orders-table';
$last = '?related_orders_page=' . $max_num_pages . '#woocommerce-subscriptions-related-orders-table';

Choose a reason for hiding this comment

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

I think there is an opportunity to extract helper method for composing query params

Copy link
Member Author

Choose a reason for hiding this comment

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

WordPress provides just such a helper, in the form of add_query_arg(), though I'm not sure it would make the code any more concise in this case. That said, I agree what we currently have doesn't feel great (though, it's simple).

Are you thinking along these sorts of lines?

$first = paginated_related_orders_url( 1 );
$prev  = paginated_related_orders_url( $prev_page );
$next  = paginated_related_orders_url( $next_page );
/* etc */

If so, though a little unconventional, what do you think of defining this as an anonymous function right here, within the template (ie $paginated_url = function () {})? This would avoid adding such a specific helper function to the public API surface.

Further alternative: we could move this entire chunk of pagination code into a child template.

Choose a reason for hiding this comment

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

I like anonymous function option, but do not insist on it :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Added in 91959bd.

@vbelolapotkov
Copy link

Lint fails flagged in CI aren't actually related to lines touched in this PR. Happy to address them if preferred (my usual default is not to do that, but, I'm flexible!).

I agree here.

@barryhughes
Copy link
Member Author

Interesting observation/thing that tripped me up: if WooCommerce Subscriptions Gifting is also active, then it overrides the myaccount/related-orders.php template with its own (so, pagination will not be present).

@barryhughes
Copy link
Member Author

@vbelolapotkov could I ask you to take one further look, particularly at these new commits?

91959bd
2c4d394

@a-danae a-danae removed their request for review April 28, 2025 01:00
Copy link

@vbelolapotkov vbelolapotkov left a comment

Choose a reason for hiding this comment

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

@barryhughes I've tested everything again, and it works as expected in case of many orders.

When tested with little number of related orders I noticed that pagination buttons are still rendered and they are clickable, which is not ideal IMO. The most annoying behavior is that on the first click the page is reloaded.

CleanShot 2025-04-28 at 11 50 13@2x

What do you think about hiding them completely (that would be my preference) when there is only one page or at least make them non-clickable?

Interesting observation/thing that tripped me up: if WooCommerce Subscriptions Gifting is also active, then it overrides the myaccount/related-orders.php template with its own (so, pagination will not be present).

Tested this as well, and you're right. I guess to fix this, we'll have to ship a template update for subs gifting. Do you plan doing this? Also are you aware of other our extensions which customize this template? I suppose we'd need to communicate this change with devs during or before release.

Side note, subs gifting list looks cleaner IMO because of the links instead of buttons. Maybe we should do the same? Let's ask designers, cc @poligilad-auto on this or any other design recommendations here.

CleanShot 2025-04-28 at 11 47 00@2x

Comment on lines 106 to 131
<div class="woocommerce-subscriptions-related-orders-pagination-links woocommerce-pagination">
<?php
$prev_page = max( 1, $page - 1 );
$next_page = min( $max_num_pages, $page + 1 );

$page_link = static function( int $page, string $type, string $label, bool $enabled = true ): string {
$base_classes = 'button woocommerce-button wp-element-button';
$classes = esc_attr( $base_classes . ' woocommerce-button--' . $type . ( $enabled ? '' : ' disabled' ) );
$url = esc_url( add_query_arg( 'related_orders_page', $page, '#woocommerce-subscriptions-related-orders-table' ) );

return "<a href='$url' class='$classes'>$label</a>";
};

// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- Our helper function takes care of escaping.
?>
<div class="pagination-links">
<?php echo $page_link( 1, 'first', _x( 'First', 'Related orders pagination', 'woocommerce-subscriptions' ), 1 !== $page ); ?>
<?php echo $page_link( $prev_page, 'prev', _x( 'Previous', 'Related orders pagination', 'woocommerce-subscriptions' ), 1 !== $page ); ?>
</div>

<div class="pagination-links">
<?php echo $page_link( $next_page, 'next', _x( 'Next', 'Related orders pagination', 'woocommerce-subscriptions' ), $page < $max_num_pages ); ?>
<?php echo $page_link( $max_num_pages, 'last', _x( 'Last', 'Related orders pagination', 'woocommerce-subscriptions' ), $page < $max_num_pages ); ?>
</div>
<?php // phpcs:enable ?>
</div>

Choose a reason for hiding this comment

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

I like this implementation a lot, well done! I've made another round of testing, and noticed that layout looks terrible on mobile devices:

CleanShot 2025-04-28 at 11 24 30@2x

Let's update markup and styling for that:

  • Replace text on buttons with "<<" for first and "<" for pervious.
  • Fix buttons alignment on the right edge.

This keeps things a little neater. We are deliberately using an anonymous function defined within the template because it's specialized and there's no need for it elsewhere: until such a need arises, it doesn't make sense to add this to the public API surface.
…View Subscription page).

This prevents bad actors from supplying a very high limit. We can consider making the value filterable in a future release, if this is something merchants/developers request.
@barryhughes barryhughes force-pushed the fix/162-my-subscriptions-related-orders-pagination branch from 2c4d394 to 947fa2f Compare April 30, 2025 23:36
This means plugins/customizations that have overridden the existing `related-orders.php` template can still benefit from pagination, so long as they update their version of the template to include all the required params.
@barryhughes
Copy link
Member Author

🎅🏼 I come bearing updates.


Replace text on buttons with "<<" for first and "<" for previous.

This has been implemented for small viewports.

Fix buttons alignment on the right edge.

Also addressed, and a small tweak made to improve things when using Storefront.

What do you think about hiding them completely (that would be my preference) when there is only one page or at least make them non-clickable?

Done, and I also hide them if they are not needed (ie, the next and last buttons are not needed if we are on the final page of the result set).

I guess to fix this, we'll have to ship a template update for subs gifting. Do you plan doing this?

I will create an issue so we don't forget (or perhaps will go ahead and create a PR since it's a small change). Inspired by this problem, though, I moved the pagination controls into their own template (and I integrate them into the related-orders template using an existing hook).

For Gifting, we'll still need to make an update, though, so that it triggers the same hook and passes along the same set of parameters.

Copy link

@vbelolapotkov vbelolapotkov left a comment

Choose a reason for hiding this comment

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

Great improvements @barryhughes, thank you! I've tested it and everything works well. I have some minor non-blocking comments, but don't want to hold this PR anymore, so approving. Feel free to merge once ready.

Styling and layout look much better now. The small thing would be nice to fix before shipping is the space between buttons and customer billing info. Right now there is very little space, especially in Storefront.

CleanShot 2025-05-01 at 14 00 21@2x

What do you think about hiding them completely (that would be my preference) when there is only one page or at least make them non-clickable?
Done, and I also hide them if they are not needed (ie, the next and last buttons are not needed if we are on the final page of the result set).

I played with it, and found UX a bit odd, especially when you have 11 orders. Those buttons just jump on the left/right. I leave the final decision up to you, but I'd show/hide all of them together.

Inspired by this problem, though, I moved the pagination controls into their own template (and I integrate them into the related-orders template using an existing hook).

I love that, thanks!

direction: rtl;
}

@media (max-width: 30em) {

Choose a reason for hiding this comment

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

@barryhughes I'd like to understand where the value comes from. The rest of the codebase uses px values (different and weird at times), so I'm wondering what's your source for this choice?

Copy link
Member Author

Choose a reason for hiding this comment

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

It looked like both units of measure are in use in this file (for instance, look at the two blocks which precede the pagination rules—both use em) though you're right that px is more common. It was really just personal choice: I feel em makes more sense when we're shaping how text is laid out. I can certainly switch to px. As to how I got to this specific value, just through experimentation with different themes since there isn't a single breakpoint that works perfectly for all.

Choose a reason for hiding this comment

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

Thanks for clarifying. I'd say go with whatever makes the most sense for you at this point, but I'd want to review our design guidelines later, and hopefully come to some more consistent approach.

Comment on lines 971 to 972
* @since 7.5.0 Updated to support pagination of the related orders list.
*

Choose a reason for hiding this comment

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

FYI, there seem to be some formatting discrepancy. Looks like spaces applied instead of tabs.

Copy link
Member Author

Choose a reason for hiding this comment

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

Apologies ... not sure how that slipped through. Should be addressed in d7d92e9.

Choose a reason for hiding this comment

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

No worries, it's hard/impossible to spot. I noticed in GitHub UI, but it looked fine in IDE until I highlighted everything :D

*
* @return void
*/
public static function get_related_orders_pagination_template( WC_Subscription $subscription, ?array $subscription_orders = null, ?int $page = null, ?int $max_num_pages = null ) {

Choose a reason for hiding this comment

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

Double-checking my understanding here. I see that $subscription and $subscription_orders are not used in pagination template. Is it here for other extensions in case they want to have more advanced pagination? If that's the case, sure let's keep it, otherwise, we can limit pagination template to only two params used page and max_num_pages.

Copy link
Member Author

Choose a reason for hiding this comment

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

The template doc block was incorrect, and implied that $subscription and $subscription_orders were in scope. That was wrong, so I've updated it:

266e9b3

Does that clarify things?

@barryhughes
Copy link
Member Author

I played with it, and found UX a bit odd, especially when you have 11 orders. Those buttons just jump on the left/right. I leave the final decision up to you, but I'd show/hide all of them together.

Yep, I agree. Updated so that they remain on the page, but they are more obviously disabled (ie, clicking on them doesn't do anything): 1ed30d2.

@barryhughes
Copy link
Member Author

The small thing would be nice to fix before shipping is the space between buttons and customer billing info. Right now there is very little space, especially in Storefront.

In Storefront specifically, a rule from the theme was taking precedence so our margins weren't being respected. I've made our own rule a little more specific (see ea8e383). This now looks fairly balanced to me, on Storefront as well as Twenty Twenty-Two/Three/Four, but let me know if you want them to be tweaked a little more.

@barryhughes
Copy link
Member Author

I have some minor non-blocking comments, but don't want to hold this PR anymore, so approving. Feel free to merge once ready.

Thanks, though definitely feel free to take another look if you have time and bandwidth.

At this point, I don't want to merge immediately (there are some structural changes unrelated to this issue that it probably makes sense to complete first), so we have time and space to keep finessing, bandwidth-permitting. Thank you, too, for all the feedback—it should result in a much more polished change 😎

@poligilad-auto
Copy link

Hey @vbelolapotkov thanks for the ping!

Side note, subs gifting list looks cleaner IMO because of the links instead of buttons. Maybe we should do the same? Let's ask designers, cc @poligilad-auto on this or any other design recommendations here.

Regarding the buttons, I think it makes sense to use buttons as "View" order / subscription are the main actions users want to take on the page. Also, it's best to keep the account area unified and that is what we are using in the "Orders" page.

I took a look and I see we are using the pagination style that is also used in the Orders page.

image

Ideally we'd used the pagination component and style that's coming from the site 👇

image

I'll check how we can raise this topic holistically for the account area.

cc @barryhughes

@barryhughes
Copy link
Member Author

I took a look and I see we are using the pagination style that is also used in the Orders page.

Thanks for taking a peek, @poligilad-auto! Yes, that was the basis for this.

Ideally we'd used the pagination component and style that's coming from the site 👇

OK. I'm not sure what's involved there or what sort of variation we might see in pagination implementations across themes, but can certainly take a look.

Would this be worth holding up the change for? I'm happy with that, to be clear, but just trying to gauge if it's something where we can ship now (there are performance and usability benefits to this work, for stores with a large number of renewal or switch orders) and iterate later on the pagination aesthetics?

@poligilad-auto
Copy link

Would this be worth holding up the change for? I'm happy with that, to be clear, but just trying to gauge if it's something where we can ship now (there are performance and usability benefits to this work, for stores with a large number of renewal or switch orders) and iterate later on the pagination aesthetics?

@barryhughes I think it’s definitely good to ship for now! We can explore that as a separate step. I’ll post this in a quick P2, I’m also not sure how it was originally implemented.

@barryhughes
Copy link
Member Author

Subscriptions Core has now been archived. For more information, please refer to this announcement.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants