Skip to content

Commit 624035b

Browse files
committed
feat(filters): adds mergeable reviewDecision composite value
1 parent 6bc382f commit 624035b

File tree

5 files changed

+34
-5
lines changed

5 files changed

+34
-5
lines changed

src/app/components/dashboard/PersonalSummaryStrip.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,7 @@ export default function PersonalSummaryStrip(props: PersonalSummaryStripProps) {
9191
// Counts are computed from unfiltered data (ignoring scope, globalFilter, showPrRuns).
9292
// Click filters set scope=all so tabs don't hide items the count included.
9393
// Known approximations (single-value filter system cannot express these):
94-
// - "ready to merge": count requires reviewDecision=APPROVED||null, but filter
95-
// can't express OR — PRs with CHANGES_REQUESTED + passing CI may appear
94+
// - "ready to merge": uses composite reviewDecision=mergeable (APPROVED||null)
9695
// - "awaiting review": count excludes self-authored PRs (!isAuthor), but
9796
// role=reviewer filter includes them if user is both author+reviewer (rare)
9897
// - globalFilter (org/repo) is NOT applied here — counts are persistent
@@ -130,6 +129,7 @@ export default function PersonalSummaryStrip(props: PersonalSummaryStripProps) {
130129
setTabFilter("pullRequests", "role", "author");
131130
setTabFilter("pullRequests", "draft", "ready");
132131
setTabFilter("pullRequests", "checkStatus", "success");
132+
setTabFilter("pullRequests", "reviewDecision", "mergeable");
133133
},
134134
});
135135
if (prsBlocked > 0) items.push({

src/app/components/dashboard/PullRequestsTab.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ const prFilterGroups: FilterChipGroupDef[] = [
8282
{ value: "APPROVED", label: "Approved" },
8383
{ value: "CHANGES_REQUESTED", label: "Changes" },
8484
{ value: "REVIEW_REQUIRED", label: "Needs review" },
85+
{ value: "mergeable", label: "Mergeable" },
8586
],
8687
},
8788
{
@@ -213,7 +214,11 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
213214
if (!isEnriched && tabFilters.role === "author" && !roles.includes("author")) return false;
214215
}
215216
if (tabFilters.reviewDecision !== "all") {
216-
if (pr.reviewDecision !== tabFilters.reviewDecision) return false;
217+
if (tabFilters.reviewDecision === "mergeable") {
218+
if (pr.reviewDecision !== "APPROVED" && pr.reviewDecision !== null) return false;
219+
} else {
220+
if (pr.reviewDecision !== tabFilters.reviewDecision) return false;
221+
}
217222
}
218223
if (tabFilters.draft !== "all") {
219224
if (tabFilters.draft === "draft" && !pr.draft) return false;

src/app/stores/view.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const IssueFiltersSchema = z.object({
1616
const PullRequestFiltersSchema = z.object({
1717
scope: z.enum(["involves_me", "all"]).default("involves_me"),
1818
role: z.enum(["all", "author", "reviewer", "assignee"]).default("all"),
19-
reviewDecision: z.enum(["all", "APPROVED", "CHANGES_REQUESTED", "REVIEW_REQUIRED"]).default("all"),
19+
reviewDecision: z.enum(["all", "APPROVED", "CHANGES_REQUESTED", "REVIEW_REQUIRED", "mergeable"]).default("all"),
2020
draft: z.enum(["all", "draft", "ready"]).default("all"),
2121
checkStatus: z.enum(["all", "success", "failure", "pending", "conflict", "blocked", "none"]).default("all"),
2222
sizeCategory: z.enum(["all", "XS", "S", "M", "L", "XL"]).default("all"),

tests/components/dashboard/PersonalSummaryStrip.test.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ describe("PersonalSummaryStrip — click applies filters", () => {
455455
expect(viewState.tabFilters.pullRequests.reviewDecision).toBe("REVIEW_REQUIRED");
456456
});
457457

458-
it("clicking ready to merge sets scope=all, role=author, draft=ready, checkStatus=success", () => {
458+
it("clicking ready to merge sets scope=all, role=author, draft=ready, checkStatus=success, reviewDecision=mergeable", () => {
459459
const onTabChange = vi.fn();
460460
const prs = [makePullRequest({ userLogin: "me", draft: false, checkStatus: "success", reviewDecision: "APPROVED" })];
461461

@@ -469,6 +469,7 @@ describe("PersonalSummaryStrip — click applies filters", () => {
469469
expect(viewState.tabFilters.pullRequests.role).toBe("author");
470470
expect(viewState.tabFilters.pullRequests.draft).toBe("ready");
471471
expect(viewState.tabFilters.pullRequests.checkStatus).toBe("success");
472+
expect(viewState.tabFilters.pullRequests.reviewDecision).toBe("mergeable");
472473
});
473474

474475
it("clicking blocked sets scope=all, role=author, draft=ready, checkStatus=blocked", () => {
@@ -507,6 +508,8 @@ describe("PersonalSummaryStrip — count-to-filter contract", () => {
507508
makePullRequest({ id: 4, title: "Review PR", repoFullName: "org/repo-c", userLogin: "other-author", draft: false, checkStatus: "pending", reviewDecision: "REVIEW_REQUIRED", surfacedBy: ["other-author"], enriched: true, reviewerLogins: ["me"] }),
508509
// PR authored by me, draft with failing CI → NOT blocked (draft excluded)
509510
makePullRequest({ id: 5, title: "Draft PR", repoFullName: "org/repo-a", userLogin: "me", draft: true, checkStatus: "failure", reviewDecision: null, surfacedBy: ["me"], enriched: true, reviewerLogins: [] }),
511+
// PR authored by me, passing CI, but CHANGES_REQUESTED → NOT ready to merge
512+
makePullRequest({ id: 7, title: "Changes Requested PR", repoFullName: "org/repo-a", userLogin: "me", draft: false, checkStatus: "success", reviewDecision: "CHANGES_REQUESTED", surfacedBy: ["me"], enriched: true, reviewerLogins: [] }),
510513
// PR from tracked user, user not involved → only visible in scope=all
511514
makePullRequest({ id: 6, title: "Tracked PR", repoFullName: "org/repo-d", userLogin: "tracked-user", draft: false, checkStatus: "success", reviewDecision: "APPROVED", surfacedBy: ["tracked-user"], enriched: true, reviewerLogins: [] }),
512515
];
@@ -598,6 +601,7 @@ describe("PersonalSummaryStrip — count-to-filter contract", () => {
598601
screen.getByText("Ready PR");
599602
expect(screen.queryByText("Conflict PR")).toBeNull();
600603
expect(screen.queryByText("Failing PR")).toBeNull();
604+
expect(screen.queryByText("Changes Requested PR")).toBeNull();
601605
});
602606

603607
it("'assigned' count matches IssuesTab filtered view", () => {

tests/components/dashboard/PullRequestsTab.test.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,3 +532,23 @@ describe("PullRequestsTab — checkStatus=blocked filter", () => {
532532
expect(screen.queryByText("Passing PR")).toBeNull();
533533
});
534534
});
535+
536+
describe("PullRequestsTab — reviewDecision=mergeable filter", () => {
537+
it("shows APPROVED and null-review PRs, excludes CHANGES_REQUESTED", () => {
538+
const prs = [
539+
makePullRequest({ id: 1, title: "Approved PR", repoFullName: "org/repo", reviewDecision: "APPROVED", surfacedBy: ["me"], enriched: true }),
540+
makePullRequest({ id: 2, title: "No Review PR", repoFullName: "org/repo", reviewDecision: null, surfacedBy: ["me"], enriched: true }),
541+
makePullRequest({ id: 3, title: "Changes PR", repoFullName: "org/repo", reviewDecision: "CHANGES_REQUESTED", surfacedBy: ["me"], enriched: true }),
542+
];
543+
setTabFilter("pullRequests", "reviewDecision", "mergeable");
544+
setAllExpanded("pullRequests", ["org/repo"], true);
545+
546+
render(() => (
547+
<PullRequestsTab pullRequests={prs} userLogin="me" monitoredRepos={[]} />
548+
));
549+
550+
screen.getByText("Approved PR");
551+
screen.getByText("No Review PR");
552+
expect(screen.queryByText("Changes PR")).toBeNull();
553+
});
554+
});

0 commit comments

Comments
 (0)