diff --git a/.server-changes/archive-branch-keeps-list-page.md b/.server-changes/archive-branch-keeps-list-page.md new file mode 100644 index 00000000000..5b5c423d867 --- /dev/null +++ b/.server-changes/archive-branch-keeps-list-page.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Archiving a branch now returns you to the same page of the branches list, keeping your place, search and filters instead of resetting to the first page. diff --git a/apps/webapp/app/routes/resources.branches.archive.tsx b/apps/webapp/app/routes/resources.branches.archive.tsx index bf9e05a0d64..3fb1d5c64b1 100644 --- a/apps/webapp/app/routes/resources.branches.archive.tsx +++ b/apps/webapp/app/routes/resources.branches.archive.tsx @@ -13,7 +13,7 @@ import { Paragraph } from "~/components/primitives/Paragraph"; import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; import { ArchiveBranchService } from "~/services/archiveBranch.server"; import { requireUserId } from "~/services/session.server"; -import { branchesDevPath, branchesPath } from "~/utils/pathBuilder"; +import { sanitizeRedirectPath } from "~/utils"; const ArchiveBranchOptions = z.object({ environmentId: z.string(), @@ -35,6 +35,8 @@ export async function action({ request }: ActionFunctionArgs) { return redirectWithErrorMessage("/", request, "Invalid form data"); } + const redirectPath = sanitizeRedirectPath(submission.value.redirectPath); + const archiveBranchService = new ArchiveBranchService(); const result = await archiveBranchService.call( @@ -46,15 +48,13 @@ export async function action({ request }: ActionFunctionArgs) { if (result.success) { return redirectWithSuccessMessage( - result.branch.type === "DEVELOPMENT" - ? branchesDevPath(result.organization, result.project, result.branch) - : branchesPath(result.organization, result.project, result.branch), + redirectPath, request, `Branch "${result.branch.branchName}" archived` ); } - return redirectWithErrorMessage(submission.value.redirectPath, request, result.error); + return redirectWithErrorMessage(redirectPath, request, result.error); } export function ArchiveButton({ diff --git a/apps/webapp/test/archiveBranchRedirect.test.ts b/apps/webapp/test/archiveBranchRedirect.test.ts new file mode 100644 index 00000000000..f096376e7d5 --- /dev/null +++ b/apps/webapp/test/archiveBranchRedirect.test.ts @@ -0,0 +1,63 @@ +// The archive dialog submits the page it was opened from, so archiving from a +// paginated or filtered branches list must land back on that exact page instead +// of a bare branches path that resets the list to page 1. + +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { action } from "~/routes/resources.branches.archive"; + +vi.mock("~/services/session.server", () => ({ + requireUserId: vi.fn().mockResolvedValue("user_1"), +})); + +const archiveSucceeds = { value: true }; + +vi.mock("~/services/archiveBranch.server", () => ({ + ArchiveBranchService: class { + async call() { + return archiveSucceeds.value + ? { success: true as const, branch: { branchName: "feat/checkout" } } + : { success: false as const, error: "Failed to archive branch" }; + } + }, +})); + +const LIST_PATH = "/orgs/o/projects/p/env/preview/branches?page=3&search=feat"; + +async function archive(redirectPath: string) { + const body = new URLSearchParams({ environmentId: "env_1", redirectPath }); + + return (await (action as any)({ + request: new Request("https://app.example.com/resources/branches/archive", { + method: "POST", + body, + }), + params: {}, + context: {}, + })) as Response; +} + +describe("archiving a branch returns to the page it was started from", () => { + beforeEach(() => { + archiveSucceeds.value = true; + }); + + it("preserves the query string on success", async () => { + const response = await archive(LIST_PATH); + + expect(response.headers.get("Location")).toBe(LIST_PATH); + }); + + it("preserves the query string on failure", async () => { + archiveSucceeds.value = false; + + const response = await archive(LIST_PATH); + + expect(response.headers.get("Location")).toBe(LIST_PATH); + }); + + it("keeps the redirect same-origin", async () => { + const response = await archive("//evil.example.com/branches"); + + expect(response.headers.get("Location")).toBe("/"); + }); +});