From 28210a0bbb319dab069fc681e20e4c3d4f476b7f Mon Sep 17 00:00:00 2001 From: Luan Taraschi <130802253+luantaraschi@users.noreply.github.com> Date: Thu, 6 Aug 2026 19:21:25 -0300 Subject: [PATCH] fix(ui): keep list query params when refreshing after a bulk edit EditMany's success handler rebuilt the URL from `useSearchParams()`, which is stale in a list view: ListQueryProvider syncs the query to the URL through the History API to avoid a re-render, and the hook never observes that. The refresh therefore navigated to a bare `?_r=`, dropping columns, filters and limit. Reading the live location keeps them. The Localizer already works around the same staleness the same way. --- .../src/elements/EditMany/DrawerContent.tsx | 6 ++- test/bulk-edit/e2e.spec.ts | 38 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/elements/EditMany/DrawerContent.tsx b/packages/ui/src/elements/EditMany/DrawerContent.tsx index c7d6fe12536..32a85d372b4 100644 --- a/packages/ui/src/elements/EditMany/DrawerContent.tsx +++ b/packages/ui/src/elements/EditMany/DrawerContent.tsx @@ -278,7 +278,11 @@ export const EditManyDrawerContent: React.FC = (prop router.replace( qs.stringify( { - ...parseSearchParams(searchParams), + // Read the live location instead of `useSearchParams()`, which is stale + // here: `ListQueryProvider` syncs the list query to the URL through the + // History API to avoid a re-render, and the hook never observes that. + // Spreading the stale value wipes columns/filters/limit off the URL. + ...parseSearchParams(new URLSearchParams(window.location.search)), _r: Date.now(), // Cache buster to force fresh data fetch. Prevents an e2e race condition where sometimes the data is not updated. page: selectAll ? '1' : undefined, }, diff --git a/test/bulk-edit/e2e.spec.ts b/test/bulk-edit/e2e.spec.ts index 86c96f9c0cf..f75f8f3838e 100644 --- a/test/bulk-edit/e2e.spec.ts +++ b/test/bulk-edit/e2e.spec.ts @@ -683,6 +683,44 @@ test.describe('Bulk Edit', () => { } }) + test('should keep the list query params in the URL after a successful edit', async () => { + await deleteAllPosts() + await createPost({ title: 'Post 1' }) + + await page.goto(postsUrl.list) + + // `ListQueryProvider` writes these through the History API rather than the + // router, so `useSearchParams()` never observes them. Waiting for `limit` + // makes that the explicit precondition instead of an incidental one. + await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('limit=') + + await page.locator('input#select-all').check() + await page.locator('.list-selection__button[aria-label="Edit"]').click() + + const editDrawer = page.locator('dialog#edit-posts') + await expect(editDrawer).toBeVisible() + + const fieldSelectControl = editDrawer.locator('.field-select .rs__control') + await expect(fieldSelectControl).toBeVisible() + await fieldSelectControl.click() + await getSelectMenu({ page }).locator('.rs__option:has-text("Title")').first().click() + + await editDrawer.locator('input#field-title').fill('test') + + // Assert on the refresh request rather than the settled URL: the provider + // re-adds the params on the next render, so the address bar self-heals and + // would hide a refresh that went out bare. + const refreshRequest = page.waitForRequest( + (request) => request.url().includes('/collections/posts') && request.url().includes('_r='), + ) + + await editDrawer.locator('button[type="submit"]:has-text("Publish changes")').click() + + await expect(page.locator('.payload-toast-container .toast-success')).toBeVisible() + + expect((await refreshRequest).url()).toContain('limit=') + }) + test('should not delete nested un-named tab array data', async () => { const originalDoc = await payload.create({ collection: tabsSlug,