From 2a3df653f7a20d722915845f8a11076981865d47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Salavisa?= Date: Mon, 27 Apr 2026 18:28:40 +0100 Subject: [PATCH] feat(filters): add 'Has PR' task sidebar filter (#713) Co-authored-by: Kandev Agent --- .../filter-dimension-registry.ts | 8 ++++++++ apps/web/lib/sidebar/apply-view.test.ts | 20 +++++++++++++++++++ apps/web/lib/sidebar/apply-view.ts | 1 + .../lib/state/slices/ui/sidebar-view-types.ts | 1 + 4 files changed, 30 insertions(+) diff --git a/apps/web/components/task/sidebar-filter/filter-dimension-registry.ts b/apps/web/components/task/sidebar-filter/filter-dimension-registry.ts index e54a0c9412..cac74f9ad1 100644 --- a/apps/web/components/task/sidebar-filter/filter-dimension-registry.ts +++ b/apps/web/components/task/sidebar-filter/filter-dimension-registry.ts @@ -52,6 +52,14 @@ export const DIMENSION_METAS: DimensionMeta[] = [ defaultOp: "is", defaultValue: true, }, + { + dimension: "hasPR", + label: "Has PR", + valueKind: "boolean", + ops: ["is", "is_not"], + defaultOp: "is", + defaultValue: true, + }, { dimension: "state", label: "State", diff --git a/apps/web/lib/sidebar/apply-view.test.ts b/apps/web/lib/sidebar/apply-view.test.ts index 2e0bd574b8..08a36fe614 100644 --- a/apps/web/lib/sidebar/apply-view.test.ts +++ b/apps/web/lib/sidebar/apply-view.test.ts @@ -21,6 +21,26 @@ describe("applyFilters — basics", () => { }); }); +describe("applyFilters — hasPR", () => { + it("filters by hasPR is true (task with linked PR)", () => { + const tasks = [task({ id: "a", prInfo: { number: 1, state: "Open" } }), task({ id: "b" })]; + const out = applyFilters(tasks, [C({ dimension: "hasPR", op: "is", value: true })]); + expect(out.map((t) => t.id)).toEqual(["a"]); + }); + + it("filters by hasPR is false (task without linked PR)", () => { + const tasks = [task({ id: "a", prInfo: { number: 1, state: "Open" } }), task({ id: "b" })]; + const out = applyFilters(tasks, [C({ dimension: "hasPR", op: "is", value: false })]); + expect(out.map((t) => t.id)).toEqual(["b"]); + }); + + it("supports is_not negation on hasPR", () => { + const tasks = [task({ id: "a", prInfo: { number: 1, state: "Open" } }), task({ id: "b" })]; + const out = applyFilters(tasks, [C({ dimension: "hasPR", op: "is_not", value: true })]); + expect(out.map((t) => t.id)).toEqual(["b"]); + }); +}); + describe("applyFilters — per-dimension", () => { it("filters by isPRReview is true (watcher-created task)", () => { const tasks = [task({ id: "a", isPRReview: true }), task({ id: "b" })]; diff --git a/apps/web/lib/sidebar/apply-view.ts b/apps/web/lib/sidebar/apply-view.ts index 54ee57bce3..1f468a31f2 100644 --- a/apps/web/lib/sidebar/apply-view.ts +++ b/apps/web/lib/sidebar/apply-view.ts @@ -46,6 +46,7 @@ const dimensionExtractors: Record = { const ds = t.diffStats; return !!ds && (ds.additions > 0 || ds.deletions > 0); }, + hasPR: (t) => !!t.prInfo, isPRReview: (t) => t.isPRReview === true, isIssueWatch: (t) => t.isIssueWatch === true, titleMatch: (t) => t.title ?? "", diff --git a/apps/web/lib/state/slices/ui/sidebar-view-types.ts b/apps/web/lib/state/slices/ui/sidebar-view-types.ts index 426a294d50..565f3f0ca1 100644 --- a/apps/web/lib/state/slices/ui/sidebar-view-types.ts +++ b/apps/web/lib/state/slices/ui/sidebar-view-types.ts @@ -6,6 +6,7 @@ export type FilterDimension = | "executorType" | "repository" | "hasDiff" + | "hasPR" | "isPRReview" | "isIssueWatch" | "titleMatch";