Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .server-changes/archive-branch-keeps-list-page.md
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 5 additions & 5 deletions apps/webapp/app/routes/resources.branches.archive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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(
Expand All @@ -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({
Expand Down
63 changes: 63 additions & 0 deletions apps/webapp/test/archiveBranchRedirect.test.ts
Original file line number Diff line number Diff line change
@@ -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" };
}
},
}));
Comment thread
claude[bot] marked this conversation as resolved.

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("/");
});
});