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 0000000000..db60e16863 --- /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 06dad762b0..e8d7ec3862 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 bf9e05a0d6..787c3b4aca 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({