Repository: wagtail/wagtail

Issue:
`next_url` can end up with the IndexResultsView when searching and then clicking on an action button that receives the `next_url`
### Issue Summary

While in the page explorer, if I search for a title and then click on an action button, the populated `next_url` includes `/results/` at the end. Action buttons that take the `next_url` includes "Copy," "Delete," and "Unpublish."

### Steps to Reproduce

1. In wagtail admin page explorer, search for a page title
2. In the results, open on of the page's listing buttons
3. Hover over "Copy," you should see `?next=/admin/pages/10000/results/`, in my screenshots you'll see: `https://localhost:7443/admin/pages/43169/copy/?next=%2Fadmin%2Fpages%2F43164%2Fresults%2F`
4. Click "Copy," change the title and slug a bit, and click "Copy this page"
5. I end up on `https://localhost:7443/admin/pages/43164/results/`

My dev screenshots:
<img width="1379" alt="search-then-copy" src="https://github.com/wagtail/wagtail/assets/5419301/a271d5f7-140f-4b04-acc5-7df29b4fc6f1">
<img width="1381" alt="copy-form-page" src="https://github.com/wagtail/wagtail/assets/5419301/fbdd6568-ebdc-43ac-a3ac-f9e4a2f819e4">
<img width="1378" alt="copy-and-delete-success-redirect" src="https://github.com/wagtail/wagtail/assets/5419301/2fd4c799-5ce1-43c5-b520-a52729edea09">
<img width="1382" alt="search-then-delete-button" src="https://github.com/wagtail/wagtail/assets/5419301/cbfb7e5d-8d01-437f-9d3b-3f9fb9691e44">
<img width="1378" alt="delete-copy-confirm" src="https://github.com/wagtail/wagtail/assets/5419301/0d1f56d2-f0f6-490a-bcae-639efb9e7d15">


Any other relevant information. For example, why do you consider this a bug and what did you expect to happen instead?

-  I have confirmed that this issue can be reproduced as described on a fresh Wagtail project: yes, in bakerydemo on GitPod

![bakerydemo-search-for-about](https://github.com/wagtail/wagtail/assets/5419301/6ff7629e-5ffd-49df-bbf1-fba236cab372)
![bakerydemo-after-copying](https://github.com/wagtail/wagtail/assets/5419301/ab67c20e-9dfb-4b7d-ba20-b928ee56fb33)

- I also opened a PR: https://github.com/wagtail/wagtail/pull/11175

### Technical details

-   Python version: 3.10.11
-   Django version: 4.2.4
-   Wagtail version: 5.2rc1
-   Browser version: Chrome 119



Discussion:
I think #11170 may actually be a more generic fix for this
Update. Seems like it's not. 
@lb- I don't think so - if I'm reading right, #11170 is simply adding `next_url` to the add/edit page actions so that they redirect back to the original location, just like move/delete etc do. It won't do anything to address next_url pointing to the wrong thing.
Confirming this is an issue. Observe in the screenshot below, the copy button's `next_url` is incorrectly set to the `/results` path.

<img width="1896" alt="Screenshot 2023-11-06 at 8 54 14 pm" src="https://github.com/wagtail/wagtail/assets/1396140/134786f4-af1d-4496-a447-044cccbeb2be">


## Code path

1. [wagtail/admin/templates/wagtailadmin/pages/listing/_page_title_explore.html](https://github.com/wagtail/wagtail/blob/main/wagtail/admin/templates/wagtailadmin/pages/listing/_page_title_explore.html#L31) uses the template tag `page_listing_buttons`
2. [`wagtail/admin/templatetags/wagtailadmin_tags.py`](https://github.com/wagtail/wagtail/blob/main/wagtail/admin/templatetags/wagtailadmin_tags.py#L447) prepares the `next_url` as `next_url = context["request"].path`
3. When we get the async response, the request path is the `/results` as per the URL `wagtail/admin/urls/__init__.py` `wagtailadmin_explore_results`
4. Changed in 30c68652c7e6d01094b681d4d49c5954296f2f7c

I am not 100% sure on a fix here, and this may indicate a bit of a rough edge to our adoption of discrete URLs for results listings instead of requesting the same URL with a header to differentiate 'partial' responses. As per the plan in https://github.com/wagtail/wagtail/issues/9272
Sorry, pushed the wrong button, comment updated now.

Resolving patch:
diff --git a/wagtail/admin/templatetags/wagtailadmin_tags.py b/wagtail/admin/templatetags/wagtailadmin_tags.py
--- a/wagtail/admin/templatetags/wagtailadmin_tags.py
+++ b/wagtail/admin/templatetags/wagtailadmin_tags.py
@@ -444,8 +444,8 @@ def paginate(context, page, base_url="", page_key="p", classname=""):
 
 
 @register.inclusion_tag("wagtailadmin/shared/buttons.html", takes_context=True)
-def page_listing_buttons(context, page, user):
-    next_url = context["request"].path
+def page_listing_buttons(context, page, user, next_url=None):
+    next_url = next_url or context["request"].path
     button_hooks = hooks.get_hooks("register_page_listing_buttons")
 
     buttons = []
diff --git a/wagtail/admin/ui/tables/pages.py b/wagtail/admin/ui/tables/pages.py
--- a/wagtail/admin/ui/tables/pages.py
+++ b/wagtail/admin/ui/tables/pages.py
@@ -27,6 +27,7 @@ def get_cell_context_data(self, instance, parent_context):
         context["parent_page"] = getattr(instance, "annotated_parent_page", None)
         context["show_locale_labels"] = parent_context.get("show_locale_labels")
         context["perms"] = parent_context.get("perms")
+        context["actions_next_url"] = parent_context.get("actions_next_url")
         return context
 
 
@@ -90,6 +91,7 @@ def __init__(
         use_row_ordering_attributes=False,
         parent_page=None,
         show_locale_labels=False,
+        actions_next_url=None,
         **kwargs,
     ):
         super().__init__(*args, **kwargs)
@@ -114,6 +116,7 @@ def __init__(
                 )
 
         self.show_locale_labels = show_locale_labels
+        self.actions_next_url = actions_next_url
 
     def get_ascending_title_text(self, column):
         return self.ascending_title_text_format % {
@@ -150,4 +153,7 @@ def get_context_data(self, parent_context):
         context["is_searching_whole_tree"] = parent_context.get(
             "is_searching_whole_tree"
         )
+        context["actions_next_url"] = (
+            self.actions_next_url or parent_context.get("request").path
+        )
         return context
diff --git a/wagtail/admin/views/pages/listing.py b/wagtail/admin/views/pages/listing.py
--- a/wagtail/admin/views/pages/listing.py
+++ b/wagtail/admin/views/pages/listing.py
@@ -232,6 +232,7 @@ def get_table_kwargs(self):
         kwargs["use_row_ordering_attributes"] = self.show_ordering_column
         kwargs["parent_page"] = self.parent_page
         kwargs["show_locale_labels"] = self.i18n_enabled and self.parent_page.is_root()
+        kwargs["actions_next_url"] = self.get_index_url()
 
         if self.show_ordering_column:
             kwargs["attrs"] = {


Test patch:
diff --git a/wagtail/admin/tests/pages/test_explorer_view.py b/wagtail/admin/tests/pages/test_explorer_view.py
--- a/wagtail/admin/tests/pages/test_explorer_view.py
+++ b/wagtail/admin/tests/pages/test_explorer_view.py
@@ -3,6 +3,7 @@
 from django.core import paginator
 from django.test import TestCase, override_settings
 from django.urls import reverse
+from django.utils.http import urlencode
 
 from wagtail import hooks
 from wagtail.admin.widgets import Button
@@ -46,9 +47,8 @@ def setUp(self):
         self.user = self.login()
 
     def test_explore(self):
-        response = self.client.get(
-            reverse("wagtailadmin_explore", args=(self.root_page.id,))
-        )
+        explore_url = reverse("wagtailadmin_explore", args=(self.root_page.id,))
+        response = self.client.get(explore_url)
         self.assertEqual(response.status_code, 200)
         self.assertTemplateUsed(response, "wagtailadmin/pages/index.html")
         self.assertEqual(self.root_page, response.context["parent_page"])
@@ -59,6 +59,38 @@ def test_explore(self):
         self.assertEqual(
             page_ids, [self.new_page.id, self.old_page.id, self.child_page.id]
         )
+        expected_new_page_copy_url = (
+            reverse("wagtailadmin_pages:copy", args=(self.new_page.id,))
+            + "?"
+            + urlencode({"next": explore_url})
+        )
+        self.assertContains(response, f'href="{expected_new_page_copy_url}"')
+
+        self.assertContains(response, "1-3 of 3")
+
+    def test_explore_results(self):
+        explore_results_url = reverse(
+            "wagtailadmin_explore_results", args=(self.root_page.id,)
+        )
+        response = self.client.get(explore_results_url)
+        self.assertEqual(response.status_code, 200)
+        self.assertTemplateUsed(response, "wagtailadmin/pages/index_results.html")
+        self.assertEqual(self.root_page, response.context["parent_page"])
+
+        page_ids = [page.id for page in response.context["pages"]]
+        self.assertEqual(
+            page_ids, [self.new_page.id, self.old_page.id, self.child_page.id]
+        )
+        # the 'next' parameter should return to the explore view, NOT
+        # the partial explore_results view
+        explore_url = reverse("wagtailadmin_explore", args=(self.root_page.id,))
+        expected_new_page_copy_url = (
+            reverse("wagtailadmin_pages:copy", args=(self.new_page.id,))
+            + "?"
+            + urlencode({"next": explore_url})
+        )
+        self.assertContains(response, f'href="{expected_new_page_copy_url}"')
+
         self.assertContains(response, "1-3 of 3")
 
     def test_explore_root(self):