Skip to content

Commit 7b916b6

Browse files
committed
feat(home): suggest 'All repos in ORG' in the Add Repo dropdown
When the user opens the Add Repo dropdown and types a bare org name (non-empty query, no `/`), surface an "All repos in \"<query>\"" suggestion above the repo search results. Selecting it adds a new org-scoped repo filter that emits `org:<name>` in the GitHub PR search query, equivalent to the `owner/repo`-based filters but scoped to the whole organization. Once the query contains a `/` the user is narrowing to a specific repo, so the org suggestion disappears from the list. The org filter is encoded in the existing RepoFilter.name field with an `__org__:` sentinel prefix (mirroring how `__all_repos__` is represented), so the existing storage shape and dedupe/enable/mode plumbing keeps working. `buildSearchQueries` gains a new branch that groups org filters by mode+authoredBy and emits `org:<name>` qualifiers (multiple orgs OR just like multiple `repo:` qualifiers). The chip renders as `org:<name>` with the same primary tint used by "All Repos". All modes stay available on org filters — unlike `__all_repos__`, `org:<name>` is already bounded, so `is:pr org:foo` is a reasonable query. Resolves #360
1 parent e7688d7 commit 7b916b6

1 file changed

Lines changed: 116 additions & 6 deletions

File tree

src/browser/components/home.tsx

Lines changed: 116 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ type FilterMode =
107107

108108
// Special constant for "All Repos" global filter
109109
const ALL_REPOS_KEY = "__all_repos__";
110+
// Prefix used to encode an org-scoped filter in the `name` field, e.g. `__org__:anthropic`.
111+
const ORG_FILTER_PREFIX = "__org__:";
110112

111113
// Repository with its filter mode
112114
interface RepoFilter {
@@ -142,6 +144,20 @@ function isAllReposFilter(filter: RepoFilter): boolean {
142144
return filter.name === ALL_REPOS_KEY;
143145
}
144146

147+
function orgFilterKey(org: string): string {
148+
return `${ORG_FILTER_PREFIX}${org}`;
149+
}
150+
151+
function isOrgFilter(filter: RepoFilter): boolean {
152+
return filter.name.startsWith(ORG_FILTER_PREFIX);
153+
}
154+
155+
function getOrgFromFilter(filter: RepoFilter): string | null {
156+
return isOrgFilter(filter)
157+
? filter.name.slice(ORG_FILTER_PREFIX.length)
158+
: null;
159+
}
160+
145161
// ============================================================================
146162
// Storage Helpers
147163
// ============================================================================
@@ -330,9 +346,12 @@ function buildSearchQueries(
330346
? `updated:<${stalledThreshold}`
331347
: undefined;
332348

333-
// Separate "All Repos" filters from specific repo filters
349+
// Separate "All Repos", org-scoped, and specific repo filters.
334350
const allReposFilters = enabledRepos.filter(isAllReposFilter);
335-
const specificRepos = enabledRepos.filter((r) => !isAllReposFilter(r));
351+
const orgFilters = enabledRepos.filter(isOrgFilter);
352+
const specificRepos = enabledRepos.filter(
353+
(r) => !isAllReposFilter(r) && !isOrgFilter(r)
354+
);
336355

337356
// Handle "All Repos" global filters (one query per mode)
338357
for (const filter of allReposFilters) {
@@ -364,6 +383,59 @@ function buildSearchQueries(
364383
}
365384
}
366385

386+
// Group org filters by mode+authoredBy. Multiple `org:` qualifiers OR just
387+
// like `repo:` qualifiers.
388+
if (orgFilters.length > 0) {
389+
const byOrgModeKey = new Map<
390+
string,
391+
{ mode: FilterMode; authoredBy?: string; orgs: string[] }
392+
>();
393+
for (const filter of orgFilters) {
394+
const org = getOrgFromFilter(filter);
395+
if (!org) continue;
396+
const key =
397+
filter.mode === "authored-by"
398+
? `${filter.mode}:${filter.authoredBy || ""}`
399+
: filter.mode;
400+
const existing = byOrgModeKey.get(key);
401+
if (existing) {
402+
existing.orgs.push(org);
403+
} else {
404+
byOrgModeKey.set(key, {
405+
mode: filter.mode,
406+
authoredBy: filter.authoredBy,
407+
orgs: [org],
408+
});
409+
}
410+
}
411+
412+
for (const [, { mode, authoredBy, orgs }] of byOrgModeKey) {
413+
if (mode === "authored-by" && !authoredBy) continue;
414+
415+
const parts = ["is:pr", "archived:false"];
416+
if (stateFilter) parts.push(stateFilter);
417+
if (stalledFilter) parts.push(stalledFilter);
418+
parts.push(...orgs.map((o) => `org:${o}`));
419+
const modeFilter = getModeFilter(mode, authoredBy);
420+
if (modeFilter) parts.push(modeFilter);
421+
queries.push(parts.join(" "));
422+
423+
if (mode === "involves") {
424+
const teams = getCachedTeams();
425+
if (teams.length > 0) {
426+
const teamParts = ["is:pr", "archived:false"];
427+
if (stateFilter) teamParts.push(stateFilter);
428+
if (stalledFilter) teamParts.push(stalledFilter);
429+
teamParts.push(...orgs.map((o) => `org:${o}`));
430+
for (const team of teams) {
431+
teamParts.push(`team-review-requested:${team.org}/${team.slug}`);
432+
}
433+
queries.push(teamParts.join(" "));
434+
}
435+
}
436+
}
437+
}
438+
367439
// Group specific repos by mode+authoredBy (for authored-by, different authors need separate queries)
368440
if (specificRepos.length > 0) {
369441
// Use a composite key: mode + authoredBy for authored-by mode
@@ -1123,12 +1195,15 @@ export function Home() {
11231195
)}
11241196
{config.repos.map((repo) => {
11251197
const isAllRepos = isAllReposFilter(repo);
1198+
const orgName = getOrgFromFilter(repo);
1199+
const isOrg = orgName !== null;
11261200
const modeOption = MODE_OPTIONS.find(
11271201
(m) => m.value === repo.mode
11281202
)!;
11291203
const isOpen = openRepoDropdown === repo.name;
11301204
const isEnabled = repo.enabled !== false;
1131-
// For "All Repos", exclude the "All PRs" mode since it would be too broad
1205+
// For "All Repos", exclude the "All PRs" mode since it would be too broad.
1206+
// Org filters keep "all" — `org:foo` is already scoped.
11321207
const availableModes = isAllRepos
11331208
? MODE_OPTIONS.filter((m) => m.value !== "all")
11341209
: MODE_OPTIONS;
@@ -1165,7 +1240,7 @@ export function Home() {
11651240
"inline-flex items-center gap-1.5 pl-2 pr-1.5 py-1 rounded-md text-xs transition-colors border cursor-pointer",
11661241
isOpen
11671242
? "bg-muted border-border"
1168-
: isAllRepos
1243+
: isAllRepos || isOrg
11691244
? isEnabled
11701245
? "bg-primary/10 border-primary/30 hover:bg-primary/20 hover:border-primary/50"
11711246
: "bg-muted/30 border-border/50 opacity-50"
@@ -1196,7 +1271,7 @@ export function Home() {
11961271
<modeOption.icon
11971272
className={cn(
11981273
"w-3 h-3",
1199-
isAllRepos
1274+
isAllRepos || isOrg
12001275
? isEnabled
12011276
? "text-primary"
12021277
: "text-muted-foreground/50"
@@ -1209,7 +1284,11 @@ export function Home() {
12091284
!isEnabled && "line-through"
12101285
)}
12111286
>
1212-
{isAllRepos ? modeOption.label : repo.name}
1287+
{isAllRepos
1288+
? modeOption.label
1289+
: isOrg
1290+
? `org:${orgName}`
1291+
: repo.name}
12131292
</span>
12141293
{repo.mode === "authored-by" && repo.authoredBy && (
12151294
<span
@@ -1520,6 +1599,37 @@ export function Home() {
15201599
</div>
15211600
</button>
15221601
)}
1602+
{/* "All repos in ORG" suggestion — shown while the query
1603+
looks like an org name (non-empty, no slash).
1604+
Deduped against any org filter already added. */}
1605+
{(() => {
1606+
const trimmed = searchQuery.trim();
1607+
if (!trimmed || trimmed.includes("/")) return null;
1608+
const orgKey = orgFilterKey(trimmed);
1609+
if (config.repos.some((r) => r.name === orgKey))
1610+
return null;
1611+
return (
1612+
<button
1613+
onMouseDown={() => {
1614+
handleAddRepo(orgKey);
1615+
setShowAddRepo(false);
1616+
}}
1617+
className="w-full flex items-center gap-2 px-3 py-2.5 hover:bg-primary/10 transition-colors text-left border-b border-border bg-primary/5"
1618+
>
1619+
<div className="w-4 h-4 rounded bg-primary/20 flex items-center justify-center shrink-0">
1620+
<Users className="w-3 h-3 text-primary" />
1621+
</div>
1622+
<div className="flex-1 min-w-0">
1623+
<span className="font-medium text-xs">
1624+
All repos in "{trimmed}"
1625+
</span>
1626+
<span className="text-[10px] text-muted-foreground ml-1.5">
1627+
org:{trimmed}
1628+
</span>
1629+
</div>
1630+
</button>
1631+
);
1632+
})()}
15231633
{searchResults.length > 0 ? (
15241634
searchResults.map((repo) => (
15251635
<button

0 commit comments

Comments
 (0)