Skip to content
Open
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
3 changes: 3 additions & 0 deletions apps/code/src/renderer/api/posthogClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1853,6 +1853,9 @@ export class PostHogAPIClient {
if (params?.suggested_reviewers) {
url.searchParams.set("suggested_reviewers", params.suggested_reviewers);
}
if (params?.repository) {
url.searchParams.set("repository", params.repository);
}

const response = await this.api.fetcher.fetch({
method: "get",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { useInboxSignalsFilterStore } from "@features/inbox/stores/inboxSignalsF
import { useInboxSignalsSidebarStore } from "@features/inbox/stores/inboxSignalsSidebarStore";
import { useInboxSourcesDialogStore } from "@features/inbox/stores/inboxSourcesDialogStore";
import {
buildRepoFilterParam,
buildSignalReportListOrdering,
buildStatusFilterParam,
buildSuggestedReviewerFilterParam,
Expand Down Expand Up @@ -56,6 +57,7 @@ export function InboxSignalsTab() {
const suggestedReviewerFilter = useInboxSignalsFilterStore(
(s) => s.suggestedReviewerFilter,
);
const repoFilter = useInboxSignalsFilterStore((s) => s.repoFilter);

// ── GitHub integration ───────────────────────────────────────────────
const { hasGithubIntegration } = useRepositoryIntegration();
Expand Down Expand Up @@ -112,13 +114,16 @@ export function InboxSignalsTab() {
suggestedReviewerFilter.length > 0
? buildSuggestedReviewerFilterParam(suggestedReviewerFilter)
: undefined,
repository:
repoFilter.length > 0 ? buildRepoFilterParam(repoFilter) : undefined,
}),
[
statusFilter,
sortField,
sortDirection,
sourceProductFilter,
suggestedReviewerFilter,
repoFilter,
],
);

Expand Down Expand Up @@ -343,6 +348,7 @@ export function InboxSignalsTab() {
const hasActiveFilters =
sourceProductFilter.length > 0 ||
suggestedReviewerFilter.length > 0 ||
repoFilter.length > 0 ||
statusFilter.length < 5;
const shouldShowTwoPane =
hasReports ||
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { useInboxSignalsFilterStore } from "@features/inbox/stores/inboxSignalsFilterStore";
import {
useRepositoryIntegration,
useUserRepositoryIntegration,
} from "@hooks/useIntegrations";
import { Check, GitBranch, MagnifyingGlass } from "@phosphor-icons/react";
import { Box, Flex, Popover, Spinner, Text } from "@radix-ui/themes";
import { useDeferredValue, useMemo, useState } from "react";

export function RepoFilterMenu() {
const [open, setOpen] = useState(false);
const [repoQuery, setRepoQuery] = useState("");
const deferredRepoQuery = useDeferredValue(repoQuery);

const { repositories: orgRepositories, isLoadingRepos: orgLoading } =
useRepositoryIntegration();
const { repositories: userRepositories, isLoadingRepos: userLoading } =
useUserRepositoryIntegration();

const repoFilter = useInboxSignalsFilterStore((s) => s.repoFilter);
const toggleRepo = useInboxSignalsFilterStore((s) => s.toggleRepo);
const setRepoFilter = useInboxSignalsFilterStore((s) => s.setRepoFilter);

// Merge org-level + user-level repos and de-duplicate (case-insensitive). The
// signal report `repo_selection` artefact stores `owner/repo` in lowercase, and
// the filter is matched case-insensitively, so we present a single canonical
// lowercase entry per repo regardless of which integration source it came from.
const availableRepos = useMemo(() => {
const seen = new Set<string>();
const out: string[] = [];
for (const repo of [...orgRepositories, ...userRepositories]) {
const key = repo.trim().toLowerCase();
if (!key || seen.has(key)) continue;
seen.add(key);
out.push(key);
}
return out.sort((a, b) => a.localeCompare(b));
}, [orgRepositories, userRepositories]);

const visibleRepos = useMemo(() => {
const trimmed = deferredRepoQuery.trim().toLowerCase();
if (!trimmed) return availableRepos;
return availableRepos.filter((r) => r.includes(trimmed));
}, [availableRepos, deferredRepoQuery]);

const isFetching = orgLoading || userLoading;
const selectedCount = repoFilter.length;
const hasSelected = selectedCount > 0;

return (
<Popover.Root
modal
open={open}
onOpenChange={(nextOpen) => {
setOpen(nextOpen);
if (!nextOpen) {
setRepoQuery("");
}
}}
>
<Popover.Trigger>
<button
type="button"
aria-label="Filter by repository"
className={`flex h-6 min-w-6 items-center justify-center gap-1 rounded-sm px-1.5 transition-colors hover:bg-gray-3 hover:text-gray-12 ${
selectedCount > 0 ? "bg-gray-3 text-gray-12" : "text-gray-10"
}`}
>
<GitBranch size={14} />
{selectedCount > 0 ? (
<span className="text-[11px] text-gray-12 leading-none">
{selectedCount}
</span>
) : null}
</button>
</Popover.Trigger>
<Popover.Content
align="end"
side="bottom"
sideOffset={6}
className="min-w-[280px] max-w-[320px] p-[8px]"
>
<Flex direction="column" gap="2">
<Flex align="center" justify="between" gap="2">
<Text className="pl-[1px] font-medium text-[13px] text-gray-10">
Repository
</Text>
{hasSelected ? (
<button
type="button"
onClick={() => setRepoFilter([])}
className="rounded-sm px-1 py-0.5 text-[11px] text-gray-10 transition-colors hover:bg-gray-3 hover:text-gray-12"
>
Clear
</button>
) : null}
</Flex>

<Flex
align="center"
gap="2"
px="2"
py="1"
className="rounded-(--radius-2) border border-(--gray-6) bg-(--color-background)"
>
<MagnifyingGlass size={12} className="shrink-0 text-gray-10" />
<input
type="text"
placeholder="Filter repos..."
value={repoQuery}
onChange={(e) => setRepoQuery(e.target.value)}
className="min-w-0 flex-1 bg-transparent text-[12px] text-gray-12 outline-none placeholder:text-gray-9"
/>
</Flex>

<Box className="max-h-[280px] overflow-y-auto">
{isFetching && availableRepos.length === 0 ? (
<Flex align="center" justify="center" py="3">
<Spinner size="1" />
</Flex>
) : visibleRepos.length === 0 ? (
<Text color="gray" className="px-1 py-2 text-[12px]">
{availableRepos.length === 0
? "No repos available."
: "No repos match."}
</Text>
) : (
<Flex direction="column">
{visibleRepos.map((repo) => {
const isSelected = repoFilter.includes(repo);
return (
<button
key={repo}
type="button"
className="flex w-full items-center justify-between rounded-sm px-1 py-1 text-left text-[13px] text-gray-12 transition-colors hover:bg-gray-3 focus-visible:bg-gray-3 focus-visible:outline-none"
onClick={() => toggleRepo(repo)}
>
<Text className="min-w-0 truncate text-[12px]">
{repo}
</Text>
<span
className="flex h-4 w-4 shrink-0 items-center justify-center text-gray-12"
aria-hidden
>
{isSelected ? <Check size={12} weight="bold" /> : null}
</span>
</button>
);
})}
</Flex>
)}
</Box>
</Flex>
</Popover.Content>
</Popover.Root>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { IS_DEV } from "@shared/constants/environment";
import type { SignalReport } from "@shared/types";
import { useState } from "react";
import { FilterSortMenu } from "./FilterSortMenu";
import { RepoFilterMenu } from "./RepoFilterMenu";
import { SuggestedReviewerFilterMenu } from "./SuggestedReviewerFilterMenu";

interface SignalsToolbarProps {
Expand Down Expand Up @@ -233,6 +234,7 @@ export function SignalsToolbar({
{!hideFilters && (
<Flex align="center" gap="1" className="shrink-0">
<SuggestedReviewerFilterMenu />
<RepoFilterMenu />
<FilterSortMenu />
</Flex>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe("inboxSignalsFilterStore", () => {
],
sourceProductFilter: [],
suggestedReviewerFilter: [],
repoFilter: [],
});
});

Expand All @@ -36,6 +37,7 @@ describe("inboxSignalsFilterStore", () => {
]);
expect(state.sourceProductFilter).toEqual([]);
expect(state.suggestedReviewerFilter).toEqual([]);
expect(state.repoFilter).toEqual([]);
});

it("setSort updates field and direction", () => {
Expand Down Expand Up @@ -106,12 +108,48 @@ describe("inboxSignalsFilterStore", () => {
]);
});

it("toggleRepo adds and removes repos and lower-cases them", () => {
useInboxSignalsFilterStore.getState().toggleRepo("PostHog/posthog");
expect(useInboxSignalsFilterStore.getState().repoFilter).toEqual([
"posthog/posthog",
]);

useInboxSignalsFilterStore.getState().toggleRepo("posthog/posthog");
expect(useInboxSignalsFilterStore.getState().repoFilter).toEqual([]);
});

it("setRepoFilter de-duplicates and lower-cases repos", () => {
useInboxSignalsFilterStore
.getState()
.setRepoFilter([
"PostHog/posthog",
"posthog/posthog-js",
"posthog/posthog",
]);

expect(useInboxSignalsFilterStore.getState().repoFilter).toEqual([
"posthog/posthog",
"posthog/posthog-js",
]);
});

it("persists repoFilter", () => {
useInboxSignalsFilterStore.getState().setRepoFilter(["posthog/posthog"]);

const raw = localStorage.getItem("inbox-signals-filter-storage");
expect(raw).toBeTruthy();
const persisted = JSON.parse(raw as string);

expect(persisted.state.repoFilter).toEqual(["posthog/posthog"]);
});

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.setRepoFilter(["posthog/posthog"]);

useInboxSignalsFilterStore.getState().resetFilters();

Expand All @@ -127,6 +165,7 @@ describe("inboxSignalsFilterStore", () => {
]);
expect(state.sourceProductFilter).toEqual([]);
expect(state.suggestedReviewerFilter).toEqual([]);
expect(state.repoFilter).toEqual([]);
});

it("resetFilters preserves sort preferences", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,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 repositories" (no filter). Stored as `owner/repo` strings (lower-case). */
repoFilter: string[];
}

interface InboxSignalsFilterActions {
Expand All @@ -49,6 +51,8 @@ interface InboxSignalsFilterActions {
toggleSourceProduct: (source: SourceProduct) => void;
toggleSuggestedReviewer: (reviewerUuid: string) => void;
setSuggestedReviewerFilter: (reviewerUuids: string[]) => void;
toggleRepo: (repo: string) => void;
setRepoFilter: (repos: string[]) => void;
/** Reset all filters when a deep link arrives so the linked report isn't hidden. */
resetFilters: () => void;
}
Expand All @@ -65,6 +69,7 @@ export const useInboxSignalsFilterStore = create<InboxSignalsFilterStore>()(
statusFilter: DEFAULT_STATUS_FILTER,
sourceProductFilter: [],
suggestedReviewerFilter: [],
repoFilter: [],
setSort: (sortField, sortDirection) => set({ sortField, sortDirection }),
setSearchQuery: (searchQuery) => set({ searchQuery }),
setStatusFilter: (statusFilter) => set({ statusFilter }),
Expand Down Expand Up @@ -96,12 +101,31 @@ export const useInboxSignalsFilterStore = create<InboxSignalsFilterStore>()(
set({
suggestedReviewerFilter: Array.from(new Set(reviewerUuids)),
}),
toggleRepo: (repo) =>
set((state) => {
const normalized = repo.trim().toLowerCase();
if (!normalized) return {};
const current = state.repoFilter;
const next = current.includes(normalized)
? current.filter((r) => r !== normalized)
: [...current, normalized];
return { repoFilter: next };
}),
setRepoFilter: (repos) =>
set({
repoFilter: Array.from(
new Set(
repos.map((repo) => repo.trim().toLowerCase()).filter(Boolean),
),
),
}),
resetFilters: () =>
set({
searchQuery: "",
statusFilter: DEFAULT_STATUS_FILTER,
sourceProductFilter: [],
suggestedReviewerFilter: [],
repoFilter: [],
}),
}),
{
Expand All @@ -112,6 +136,7 @@ export const useInboxSignalsFilterStore = create<InboxSignalsFilterStore>()(
statusFilter: state.statusFilter,
sourceProductFilter: state.sourceProductFilter,
suggestedReviewerFilter: state.suggestedReviewerFilter,
repoFilter: state.repoFilter,
}),
},
),
Expand Down
Loading
Loading