From bfc47af27ee8716cf2c98d705489da6f637d35a8 Mon Sep 17 00:00:00 2001 From: Oliver Browne Date: Mon, 18 May 2026 11:23:33 +0000 Subject: [PATCH 1/2] feat(inbox): filter reports by priority Adds a Priority section (P0-P4) to the inbox filter popover, mirroring the existing source-product filter pattern. Selections are persisted alongside the other filter prefs and sent to the new `priority` query parameter on the signals reports list endpoint. Backend support shipped in PostHog/posthog#58784. Generated-By: PostHog Code Task-Id: a20149cc-595e-49b3-b2a9-e12641b73dbc --- apps/code/src/renderer/api/posthogClient.ts | 3 ++ .../inbox/components/InboxSignalsTab.tsx | 5 +++ .../inbox/components/list/FilterSortMenu.tsx | 40 ++++++++++++++++++ .../stores/inboxSignalsFilterStore.test.ts | 41 +++++++++++++++++++ .../inbox/stores/inboxSignalsFilterStore.ts | 20 +++++++++ .../inbox/utils/filterReports.test.ts | 17 ++++++++ .../features/inbox/utils/filterReports.ts | 10 +++++ apps/code/src/shared/types.ts | 2 + 8 files changed, 138 insertions(+) diff --git a/apps/code/src/renderer/api/posthogClient.ts b/apps/code/src/renderer/api/posthogClient.ts index 011494253..388465b2b 100644 --- a/apps/code/src/renderer/api/posthogClient.ts +++ b/apps/code/src/renderer/api/posthogClient.ts @@ -1898,6 +1898,9 @@ export class PostHogAPIClient { if (params?.suggested_reviewers) { url.searchParams.set("suggested_reviewers", params.suggested_reviewers); } + if (params?.priority) { + url.searchParams.set("priority", params.priority); + } const response = await this.api.fetcher.fetch({ method: "get", diff --git a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx index a39d37418..d05435d81 100644 --- a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx +++ b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx @@ -22,6 +22,7 @@ import { useInboxSignalsFilterStore } from "@features/inbox/stores/inboxSignalsF import { useInboxSignalsSidebarStore } from "@features/inbox/stores/inboxSignalsSidebarStore"; import { useInboxSourcesDialogStore } from "@features/inbox/stores/inboxSourcesDialogStore"; import { + buildPriorityFilterParam, buildSignalReportListOrdering, buildStatusFilterParam, buildSuggestedReviewerFilterParam, @@ -66,6 +67,7 @@ export function InboxSignalsTab() { const suggestedReviewerFilter = useInboxSignalsFilterStore( (s) => s.suggestedReviewerFilter, ); + const priorityFilter = useInboxSignalsFilterStore((s) => s.priorityFilter); // ── GitHub integration ─────────────────────────────────────────────── const { hasGithubIntegration } = useRepositoryIntegration(); @@ -122,6 +124,7 @@ export function InboxSignalsTab() { suggestedReviewerFilter.length > 0 ? buildSuggestedReviewerFilterParam(suggestedReviewerFilter) : undefined, + priority: buildPriorityFilterParam(priorityFilter), }), [ statusFilter, @@ -129,6 +132,7 @@ export function InboxSignalsTab() { sortDirection, sourceProductFilter, suggestedReviewerFilter, + priorityFilter, ], ); @@ -386,6 +390,7 @@ export function InboxSignalsTab() { const hasActiveFilters = sourceProductFilter.length > 0 || suggestedReviewerFilter.length > 0 || + priorityFilter.length > 0 || statusFilter.length < 5; const shouldShowTwoPane = hasReports || diff --git a/apps/code/src/renderer/features/inbox/components/list/FilterSortMenu.tsx b/apps/code/src/renderer/features/inbox/components/list/FilterSortMenu.tsx index e383772de..ef53691d0 100644 --- a/apps/code/src/renderer/features/inbox/components/list/FilterSortMenu.tsx +++ b/apps/code/src/renderer/features/inbox/components/list/FilterSortMenu.tsx @@ -24,6 +24,7 @@ import { import { Box, Flex, Popover, Text } from "@radix-ui/themes"; import type { SignalReportOrderingField, + SignalReportPriority, SignalReportStatus, } from "@shared/types"; import type React from "react"; @@ -75,6 +76,14 @@ const FILTERABLE_STATUSES: SignalReportStatus[] = [ "potential", ]; +const PRIORITY_OPTIONS: { value: SignalReportPriority; accent: string }[] = [ + { value: "P0", accent: "var(--red-9)" }, + { value: "P1", accent: "var(--orange-9)" }, + { value: "P2", accent: "var(--amber-9)" }, + { value: "P3", accent: "var(--gray-9)" }, + { value: "P4", accent: "var(--gray-9)" }, +]; + const SOURCE_PRODUCT_OPTIONS: { value: SourceProduct; label: string; @@ -120,6 +129,8 @@ export function FilterSortMenu() { const toggleSourceProduct = useInboxSignalsFilterStore( (s) => s.toggleSourceProduct, ); + const priorityFilter = useInboxSignalsFilterStore((s) => s.priorityFilter); + const togglePriority = useInboxSignalsFilterStore((s) => s.togglePriority); const handleContentKeyDown = (e: KeyboardEvent) => { if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return; @@ -195,6 +206,35 @@ export function FilterSortMenu() { + + + Priority + + + {PRIORITY_OPTIONS.map((option) => { + const isActive = priorityFilter.includes(option.value); + + return ( + + ); + })} + + + Status diff --git a/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.test.ts b/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.test.ts index 00693aa44..5ba194c2b 100644 --- a/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.test.ts +++ b/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.test.ts @@ -18,6 +18,7 @@ describe("inboxSignalsFilterStore", () => { ], sourceProductFilter: [], suggestedReviewerFilter: [], + priorityFilter: [], }); }); @@ -36,6 +37,7 @@ describe("inboxSignalsFilterStore", () => { ]); expect(state.sourceProductFilter).toEqual([]); expect(state.suggestedReviewerFilter).toEqual([]); + expect(state.priorityFilter).toEqual([]); }); it("setSort updates field and direction", () => { @@ -106,12 +108,50 @@ describe("inboxSignalsFilterStore", () => { ]); }); + it("togglePriority adds and removes priorities", () => { + useInboxSignalsFilterStore.getState().togglePriority("P0"); + expect(useInboxSignalsFilterStore.getState().priorityFilter).toEqual([ + "P0", + ]); + + useInboxSignalsFilterStore.getState().togglePriority("P1"); + expect(useInboxSignalsFilterStore.getState().priorityFilter).toEqual([ + "P0", + "P1", + ]); + + useInboxSignalsFilterStore.getState().togglePriority("P0"); + expect(useInboxSignalsFilterStore.getState().priorityFilter).toEqual([ + "P1", + ]); + }); + + it("setPriorityFilter de-duplicates priorities", () => { + useInboxSignalsFilterStore.getState().setPriorityFilter(["P0", "P1", "P0"]); + + expect(useInboxSignalsFilterStore.getState().priorityFilter).toEqual([ + "P0", + "P1", + ]); + }); + + it("persists priorityFilter", () => { + useInboxSignalsFilterStore.getState().setPriorityFilter(["P0", "P1"]); + + const raw = localStorage.getItem("inbox-signals-filter-storage"); + expect(raw).toBeTruthy(); + const persisted = JSON.parse(raw as string); + + expect(persisted.state.priorityFilter).toEqual(["P0", "P1"]); + }); + it("resetFilters restores defaults across all filter fields", () => { const store = useInboxSignalsFilterStore.getState(); store.setSearchQuery("hello"); store.setStatusFilter(["ready"]); store.toggleSourceProduct("github"); store.setSuggestedReviewerFilter(["reviewer-1"]); + store.setPriorityFilter(["P0", "P1"]); useInboxSignalsFilterStore.getState().resetFilters(); @@ -127,6 +167,7 @@ describe("inboxSignalsFilterStore", () => { ]); expect(state.sourceProductFilter).toEqual([]); expect(state.suggestedReviewerFilter).toEqual([]); + expect(state.priorityFilter).toEqual([]); }); it("resetFilters preserves sort preferences", () => { diff --git a/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts b/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts index c53c4c9e5..9fa8a99a8 100644 --- a/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts +++ b/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts @@ -1,5 +1,6 @@ import type { SignalReportOrderingField, + SignalReportPriority, SignalReportStatus, } from "@shared/types"; import { create } from "zustand"; @@ -39,6 +40,8 @@ interface InboxSignalsFilterState { sourceProductFilter: SourceProduct[]; /** Empty array means "all suggested reviewers" (no filter). Stored as PostHog user UUID strings. */ suggestedReviewerFilter: string[]; + /** Empty array means "all priorities" (no filter). */ + priorityFilter: SignalReportPriority[]; } interface InboxSignalsFilterActions { @@ -49,6 +52,8 @@ interface InboxSignalsFilterActions { toggleSourceProduct: (source: SourceProduct) => void; toggleSuggestedReviewer: (reviewerUuid: string) => void; setSuggestedReviewerFilter: (reviewerUuids: string[]) => void; + togglePriority: (priority: SignalReportPriority) => void; + setPriorityFilter: (priorities: SignalReportPriority[]) => void; /** Reset all filters when a deep link arrives so the linked report isn't hidden. */ resetFilters: () => void; } @@ -65,6 +70,7 @@ export const useInboxSignalsFilterStore = create()( statusFilter: DEFAULT_STATUS_FILTER, sourceProductFilter: [], suggestedReviewerFilter: [], + priorityFilter: [], setSort: (sortField, sortDirection) => set({ sortField, sortDirection }), setSearchQuery: (searchQuery) => set({ searchQuery }), setStatusFilter: (statusFilter) => set({ statusFilter }), @@ -96,12 +102,25 @@ export const useInboxSignalsFilterStore = create()( set({ suggestedReviewerFilter: Array.from(new Set(reviewerUuids)), }), + togglePriority: (priority) => + set((state) => { + const current = state.priorityFilter; + const next = current.includes(priority) + ? current.filter((p) => p !== priority) + : [...current, priority]; + return { priorityFilter: next }; + }), + setPriorityFilter: (priorities) => + set({ + priorityFilter: Array.from(new Set(priorities)), + }), resetFilters: () => set({ searchQuery: "", statusFilter: DEFAULT_STATUS_FILTER, sourceProductFilter: [], suggestedReviewerFilter: [], + priorityFilter: [], }), }), { @@ -112,6 +131,7 @@ export const useInboxSignalsFilterStore = create()( statusFilter: state.statusFilter, sourceProductFilter: state.sourceProductFilter, suggestedReviewerFilter: state.suggestedReviewerFilter, + priorityFilter: state.priorityFilter, }), }, ), diff --git a/apps/code/src/renderer/features/inbox/utils/filterReports.test.ts b/apps/code/src/renderer/features/inbox/utils/filterReports.test.ts index 6042daec0..109708204 100644 --- a/apps/code/src/renderer/features/inbox/utils/filterReports.test.ts +++ b/apps/code/src/renderer/features/inbox/utils/filterReports.test.ts @@ -1,6 +1,7 @@ import type { SignalReport } from "@shared/types"; import { describe, expect, it } from "vitest"; import { + buildPriorityFilterParam, buildSignalReportListOrdering, buildSuggestedReviewerFilterParam, filterReportsBySearch, @@ -153,3 +154,19 @@ describe("buildSuggestedReviewerFilterParam", () => { ).toBe("reviewer-1,reviewer-2"); }); }); + +describe("buildPriorityFilterParam", () => { + it("returns undefined for an empty array", () => { + expect(buildPriorityFilterParam([])).toBeUndefined(); + }); + + it("joins priorities with commas", () => { + expect(buildPriorityFilterParam(["P0", "P1", "P2"])).toBe("P0,P1,P2"); + }); + + it("deduplicates priorities", () => { + expect(buildPriorityFilterParam(["P0", "P1", "P0", "P2", "P1"])).toBe( + "P0,P1,P2", + ); + }); +}); diff --git a/apps/code/src/renderer/features/inbox/utils/filterReports.ts b/apps/code/src/renderer/features/inbox/utils/filterReports.ts index 82848f4ae..e9f507812 100644 --- a/apps/code/src/renderer/features/inbox/utils/filterReports.ts +++ b/apps/code/src/renderer/features/inbox/utils/filterReports.ts @@ -1,6 +1,7 @@ import type { SignalReport, SignalReportOrderingField, + SignalReportPriority, SignalReportStatus, } from "@shared/types"; @@ -70,3 +71,12 @@ export function buildSuggestedReviewerFilterParam( return Array.from(new Set(normalizedIds)).join(","); } + +export function buildPriorityFilterParam( + priorities: SignalReportPriority[], +): string | undefined { + if (priorities.length === 0) { + return undefined; + } + return Array.from(new Set(priorities)).join(","); +} diff --git a/apps/code/src/shared/types.ts b/apps/code/src/shared/types.ts index fc58146b8..7a7ec95e2 100644 --- a/apps/code/src/shared/types.ts +++ b/apps/code/src/shared/types.ts @@ -494,6 +494,8 @@ export interface SignalReportsQueryParams { source_product?: string; /** Comma-separated PostHog user UUIDs — only returns reports with these suggested reviewers. */ suggested_reviewers?: string; + /** Comma-separated `P0`–`P4` priorities — only returns reports with one of these priorities. */ + priority?: string; } /** Values match `SignalReportTask.Relationship` on the PostHog API. */ From f2962135d2cedf1eeb1c230103401e77848f8c3b Mon Sep 17 00:00:00 2001 From: Oliver Browne Date: Mon, 18 May 2026 11:38:50 +0000 Subject: [PATCH 2/2] test(inbox): parameterize buildPriorityFilterParam cases Addresses Greptile review feedback on #2176. Generated-By: PostHog Code Task-Id: a20149cc-595e-49b3-b2a9-e12641b73dbc --- .../inbox/utils/filterReports.test.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/code/src/renderer/features/inbox/utils/filterReports.test.ts b/apps/code/src/renderer/features/inbox/utils/filterReports.test.ts index 109708204..aa3a249d4 100644 --- a/apps/code/src/renderer/features/inbox/utils/filterReports.test.ts +++ b/apps/code/src/renderer/features/inbox/utils/filterReports.test.ts @@ -1,4 +1,4 @@ -import type { SignalReport } from "@shared/types"; +import type { SignalReport, SignalReportPriority } from "@shared/types"; import { describe, expect, it } from "vitest"; import { buildPriorityFilterParam, @@ -156,17 +156,17 @@ describe("buildSuggestedReviewerFilterParam", () => { }); describe("buildPriorityFilterParam", () => { - it("returns undefined for an empty array", () => { - expect(buildPriorityFilterParam([])).toBeUndefined(); - }); - - it("joins priorities with commas", () => { - expect(buildPriorityFilterParam(["P0", "P1", "P2"])).toBe("P0,P1,P2"); - }); - - it("deduplicates priorities", () => { - expect(buildPriorityFilterParam(["P0", "P1", "P0", "P2", "P1"])).toBe( - "P0,P1,P2", - ); + it.each([ + { input: [] as SignalReportPriority[], expected: undefined }, + { + input: ["P0", "P1", "P2"] as SignalReportPriority[], + expected: "P0,P1,P2", + }, + { + input: ["P0", "P1", "P0", "P2", "P1"] as SignalReportPriority[], + expected: "P0,P1,P2", + }, + ])("buildPriorityFilterParam($input) → $expected", ({ input, expected }) => { + expect(buildPriorityFilterParam(input)).toBe(expected); }); });