Skip to content

Commit 4474718

Browse files
claude[bot]claude
andauthored
fix(webapp): keep the branches list query string when archiving a branch (#4724)
<!-- ccr-slack-attribution --> _Requested by **Iss** · [Slack thread](https://triggerdotdev.slack.com/archives/C045W9WM3E1/p1787161814493949)_ **Before:** archiving a branch dropped the query string on the way back to the branches list, so the list reset to page 1. Working down a long list meant re-navigating to the page you were on after every archive. **After:** you land back on the exact page you archived from, with `page`, `search` and `showArchived` intact. The archive action now redirects to the page the request came from instead of rebuilding a bare branches path. ## How The archive dialog already submits the page it was opened from as a hidden `redirectPath` field (`${location.pathname}${location.search}`), and the failure path already redirected to it — only the success path ignored it and rebuilt the path with `branchesPath`/`branchesDevPath`, which have no query string. Both paths now redirect to the submitted path, run through the existing `sanitizeRedirectPath` helper to keep the redirect same-origin (the same idiom used by `resources.batches.$batchId.check-completion`). ## ✅ Checklist - [x] I have followed every step in the [contributing guide](https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md) - [x] The PR title follows the convention. - [x] I ran and tested the code works --- ## Testing Three files change: - `apps/webapp/app/routes/resources.branches.archive.tsx` — the fix. - `apps/webapp/test/archiveBranchRedirect.test.ts` — new test that drives the archive action and asserts the redirect `Location`: the query string survives on both success and failure, and an off-origin `redirectPath` falls back to `/`. Reverting the fix makes two of the three cases fail, so the test covers the regression. - `.server-changes/archive-branch-keeps-list-page.md` — release-note entry, since this is a user-facing server-only change. Also ran `pnpm run typecheck` and `oxlint` for `apps/webapp` — both clean. --- ## Changelog Archiving a branch now returns you to the same page of the branches list instead of resetting it to page 1. --- ## Screenshots _None — no visual change._ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent aa17c4d commit 4474718

3 files changed

Lines changed: 74 additions & 5 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
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.

apps/webapp/app/routes/resources.branches.archive.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { Paragraph } from "~/components/primitives/Paragraph";
1313
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
1414
import { ArchiveBranchService } from "~/services/archiveBranch.server";
1515
import { requireUserId } from "~/services/session.server";
16-
import { branchesDevPath, branchesPath } from "~/utils/pathBuilder";
16+
import { sanitizeRedirectPath } from "~/utils";
1717

1818
const ArchiveBranchOptions = z.object({
1919
environmentId: z.string(),
@@ -35,6 +35,8 @@ export async function action({ request }: ActionFunctionArgs) {
3535
return redirectWithErrorMessage("/", request, "Invalid form data");
3636
}
3737

38+
const redirectPath = sanitizeRedirectPath(submission.value.redirectPath);
39+
3840
const archiveBranchService = new ArchiveBranchService();
3941

4042
const result = await archiveBranchService.call(
@@ -46,15 +48,13 @@ export async function action({ request }: ActionFunctionArgs) {
4648

4749
if (result.success) {
4850
return redirectWithSuccessMessage(
49-
result.branch.type === "DEVELOPMENT"
50-
? branchesDevPath(result.organization, result.project, result.branch)
51-
: branchesPath(result.organization, result.project, result.branch),
51+
redirectPath,
5252
request,
5353
`Branch "${result.branch.branchName}" archived`
5454
);
5555
}
5656

57-
return redirectWithErrorMessage(submission.value.redirectPath, request, result.error);
57+
return redirectWithErrorMessage(redirectPath, request, result.error);
5858
}
5959

6060
export function ArchiveButton({
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// The archive dialog submits the page it was opened from, so archiving from a
2+
// paginated or filtered branches list must land back on that exact page instead
3+
// of a bare branches path that resets the list to page 1.
4+
5+
import { beforeEach, describe, expect, it, vi } from "vitest";
6+
import { action } from "~/routes/resources.branches.archive";
7+
8+
vi.mock("~/services/session.server", () => ({
9+
requireUserId: vi.fn().mockResolvedValue("user_1"),
10+
}));
11+
12+
const archiveSucceeds = { value: true };
13+
14+
vi.mock("~/services/archiveBranch.server", () => ({
15+
ArchiveBranchService: class {
16+
async call() {
17+
return archiveSucceeds.value
18+
? { success: true as const, branch: { branchName: "feat/checkout" } }
19+
: { success: false as const, error: "Failed to archive branch" };
20+
}
21+
},
22+
}));
23+
24+
const LIST_PATH = "/orgs/o/projects/p/env/preview/branches?page=3&search=feat";
25+
26+
async function archive(redirectPath: string) {
27+
const body = new URLSearchParams({ environmentId: "env_1", redirectPath });
28+
29+
return (await (action as any)({
30+
request: new Request("https://app.example.com/resources/branches/archive", {
31+
method: "POST",
32+
body,
33+
}),
34+
params: {},
35+
context: {},
36+
})) as Response;
37+
}
38+
39+
describe("archiving a branch returns to the page it was started from", () => {
40+
beforeEach(() => {
41+
archiveSucceeds.value = true;
42+
});
43+
44+
it("preserves the query string on success", async () => {
45+
const response = await archive(LIST_PATH);
46+
47+
expect(response.headers.get("Location")).toBe(LIST_PATH);
48+
});
49+
50+
it("preserves the query string on failure", async () => {
51+
archiveSucceeds.value = false;
52+
53+
const response = await archive(LIST_PATH);
54+
55+
expect(response.headers.get("Location")).toBe(LIST_PATH);
56+
});
57+
58+
it("keeps the redirect same-origin", async () => {
59+
const response = await archive("//evil.example.com/branches");
60+
61+
expect(response.headers.get("Location")).toBe("/");
62+
});
63+
});

0 commit comments

Comments
 (0)