From 42f9d2e890fb6836f25e680b11acf3b025368a99 Mon Sep 17 00:00:00 2001 From: isshaddad Date: Wed, 19 Aug 2026 13:27:30 -0400 Subject: [PATCH] fix(webapp): keep branches list pagination when archiving a branch The archive action collected the current path and query string as redirectPath, then only used it on the failure branch. On success it redirected to a freshly built branches path with no search params, so the user was thrown back to the first page after every archive. Also clamp an out-of-range page to the last page with results, since archiving shrinks the list and a restored page number can now point past the end. --- ...preserve-branches-pagination-on-archive.md | 6 ++++++ .../presenters/v3/BranchesPresenter.server.ts | 10 +++++++--- .../app/routes/resources.branches.archive.tsx | 19 ++++++++++++++++--- 3 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 .server-changes/preserve-branches-pagination-on-archive.md diff --git a/.server-changes/preserve-branches-pagination-on-archive.md b/.server-changes/preserve-branches-pagination-on-archive.md new file mode 100644 index 00000000000..db60e168630 --- /dev/null +++ b/.server-changes/preserve-branches-pagination-on-archive.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Archiving a preview branch now returns you to the same page of the branches list instead of resetting to the first page. diff --git a/apps/webapp/app/presenters/v3/BranchesPresenter.server.ts b/apps/webapp/app/presenters/v3/BranchesPresenter.server.ts index 06dad762b0c..e8d7ec3862c 100644 --- a/apps/webapp/app/presenters/v3/BranchesPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/BranchesPresenter.server.ts @@ -180,6 +180,10 @@ export class BranchesPresenter { }, }); + // Archiving shrinks the list, so a restored page number can point past the end. + const totalPages = Math.ceil(visibleCount / BRANCHES_PER_PAGE); + const currentPage = totalPages > 0 ? Math.min(page, totalPages) : 1; + const limits = await checkBranchLimit({ prisma: this.#prismaClient, organizationId: project.organizationId, @@ -222,7 +226,7 @@ export class BranchesPresenter { orderBy: { branchName: "asc", }, - skip: (page - 1) * BRANCHES_PER_PAGE, + skip: (currentPage - 1) * BRANCHES_PER_PAGE, take: BRANCHES_PER_PAGE, }); @@ -252,8 +256,8 @@ export class BranchesPresenter { return { branchableEnvironment, - currentPage: page, - totalPages: Math.ceil(visibleCount / BRANCHES_PER_PAGE), + currentPage, + totalPages, hasBranches: totalBranches > 0, branches: branchesSorted, hasFilters, diff --git a/apps/webapp/app/routes/resources.branches.archive.tsx b/apps/webapp/app/routes/resources.branches.archive.tsx index bf9e05a0d64..787c3b4aca2 100644 --- a/apps/webapp/app/routes/resources.branches.archive.tsx +++ b/apps/webapp/app/routes/resources.branches.archive.tsx @@ -25,6 +25,11 @@ const schema = ArchiveBranchOptions.and( }) ); +// Only same-origin paths are safe to redirect to, since redirectPath comes from the form. +function internalRedirectPath(path: string): string | undefined { + return path.startsWith("/") && !path.startsWith("//") ? path : undefined; +} + export async function action({ request }: ActionFunctionArgs) { const userId = await requireUserId(request); @@ -45,16 +50,24 @@ export async function action({ request }: ActionFunctionArgs) { ); if (result.success) { - return redirectWithSuccessMessage( + const listPath = result.branch.type === "DEVELOPMENT" ? branchesDevPath(result.organization, result.project, result.branch) - : branchesPath(result.organization, result.project, result.branch), + : branchesPath(result.organization, result.project, result.branch); + + // Return to the page the user archived from, so filters and pagination survive. + return redirectWithSuccessMessage( + internalRedirectPath(submission.value.redirectPath) ?? listPath, request, `Branch "${result.branch.branchName}" archived` ); } - return redirectWithErrorMessage(submission.value.redirectPath, request, result.error); + return redirectWithErrorMessage( + internalRedirectPath(submission.value.redirectPath) ?? "/", + request, + result.error + ); } export function ArchiveButton({