Skip to content

Commit 93165f7

Browse files
committed
Add repo filter for created by user
1 parent f15c141 commit 93165f7

File tree

1 file changed

+126
-37
lines changed

1 file changed

+126
-37
lines changed

src/browser/components/home.tsx

Lines changed: 126 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ type FilterMode =
6464
| "review-requested"
6565
| "reviewed"
6666
| "authored"
67+
| "authored-by"
6768
| "involves"
6869
| "all";
6970

@@ -74,6 +75,7 @@ const ALL_REPOS_KEY = "__all_repos__";
7475
interface RepoFilter {
7576
name: string;
7677
mode: FilterMode;
78+
authoredBy?: string; // Username for "authored-by" filter mode
7779
}
7880

7981
// Filter configuration stored in localStorage
@@ -136,14 +138,16 @@ function saveFilterConfig(config: FilterConfig): void {
136138
// Query Builder
137139
// ============================================================================
138140

139-
function getModeFilter(mode: FilterMode): string {
141+
function getModeFilter(mode: FilterMode, authoredBy?: string): string {
140142
switch (mode) {
141143
case "review-requested":
142144
return "review-requested:@me";
143145
case "reviewed":
144146
return "reviewed-by:@me";
145147
case "authored":
146148
return "author:@me";
149+
case "authored-by":
150+
return authoredBy ? `author:${authoredBy}` : "";
147151
case "involves":
148152
return "involves:@me";
149153
default:
@@ -175,29 +179,38 @@ function buildSearchQueries(config: FilterConfig): string[] {
175179
for (const filter of allReposFilters) {
176180
const parts = ["is:pr", "archived:false"];
177181
if (stateFilter) parts.push(stateFilter);
178-
const modeFilter = getModeFilter(filter.mode);
182+
const modeFilter = getModeFilter(filter.mode, filter.authoredBy);
179183
if (modeFilter) parts.push(modeFilter);
180184
// Note: "all" mode on All Repos would be too broad, so we skip it
181-
if (filter.mode !== "all") {
185+
// Also skip "authored-by" without a username
186+
if (filter.mode !== "all" && !(filter.mode === "authored-by" && !filter.authoredBy)) {
182187
queries.push(parts.join(" "));
183188
}
184189
}
185190

186-
// Group specific repos by mode
191+
// Group specific repos by mode+authoredBy (for authored-by, different authors need separate queries)
187192
if (specificRepos.length > 0) {
188-
const byMode = new Map<FilterMode, string[]>();
193+
// Use a composite key: mode + authoredBy for authored-by mode
194+
const byModeKey = new Map<string, { mode: FilterMode; authoredBy?: string; repos: string[] }>();
189195
for (const repo of specificRepos) {
190-
const existing = byMode.get(repo.mode) || [];
191-
existing.push(repo.name);
192-
byMode.set(repo.mode, existing);
196+
const key = repo.mode === "authored-by" ? `${repo.mode}:${repo.authoredBy || ""}` : repo.mode;
197+
const existing = byModeKey.get(key);
198+
if (existing) {
199+
existing.repos.push(repo.name);
200+
} else {
201+
byModeKey.set(key, { mode: repo.mode, authoredBy: repo.authoredBy, repos: [repo.name] });
202+
}
193203
}
194204

195-
for (const [mode, repos] of byMode) {
205+
for (const [, { mode, authoredBy, repos }] of byModeKey) {
206+
// Skip authored-by without a username
207+
if (mode === "authored-by" && !authoredBy) continue;
208+
196209
const parts = ["is:pr", "archived:false"];
197210
if (stateFilter) parts.push(stateFilter);
198211
// Multiple repo: qualifiers act as OR
199212
parts.push(...repos.map((r) => `repo:${r}`));
200-
const modeFilter = getModeFilter(mode);
213+
const modeFilter = getModeFilter(mode, authoredBy);
201214
if (modeFilter) parts.push(modeFilter);
202215
queries.push(parts.join(" "));
203216
}
@@ -258,6 +271,13 @@ const MODE_OPTIONS = [
258271
icon: User,
259272
description: "PRs you authored",
260273
},
274+
{
275+
value: "authored-by",
276+
label: "Created by User",
277+
icon: User,
278+
description: "PRs created by a specific user",
279+
hasInput: true,
280+
},
261281
{
262282
value: "involves",
263283
label: "Involves Me",
@@ -388,11 +408,11 @@ export function Home() {
388408
}, []);
389409

390410
const handleRepoModeChange = useCallback(
391-
(repoName: string, mode: FilterMode) => {
411+
(repoName: string, mode: FilterMode, authoredBy?: string) => {
392412
setConfig((prev) => ({
393413
...prev,
394414
repos: prev.repos.map((r) =>
395-
r.name === repoName ? { ...r, mode } : r
415+
r.name === repoName ? { ...r, mode, authoredBy } : r
396416
),
397417
}));
398418
},
@@ -418,6 +438,9 @@ export function Home() {
418438
top: 0,
419439
left: 0,
420440
});
441+
// Track author input for "authored-by" mode
442+
const [authoredByInput, setAuthoredByInput] = useState<string>("");
443+
const [showAuthoredByInput, setShowAuthoredByInput] = useState<string | null>(null);
421444
const [showAddRepo, setShowAddRepo] = useState(false);
422445
const [addRepoButtonRef, setAddRepoButtonRef] =
423446
useState<HTMLButtonElement | null>(null);
@@ -534,6 +557,11 @@ export function Home() {
534557
<span className={isAllRepos ? "font-medium" : "font-mono"}>
535558
{isAllRepos ? "All Repos" : repo.name}
536559
</span>
560+
{repo.mode === "authored-by" && repo.authoredBy && (
561+
<span className="text-muted-foreground">
562+
@{repo.authoredBy}
563+
</span>
564+
)}
537565
<ChevronDown className="w-3 h-3 text-muted-foreground" />
538566
<button
539567
onClick={(e) => {
@@ -551,7 +579,11 @@ export function Home() {
551579
{/* Backdrop to close dropdown when clicking outside */}
552580
<div
553581
className="fixed inset-0 z-40"
554-
onClick={() => setOpenRepoDropdown(null)}
582+
onClick={() => {
583+
setOpenRepoDropdown(null);
584+
setShowAuthoredByInput(null);
585+
setAuthoredByInput("");
586+
}}
555587
/>
556588
<div
557589
className="fixed w-56 bg-card border border-border rounded-lg shadow-xl z-50 max-w-[calc(100vw-1rem)] sm:max-w-none"
@@ -560,32 +592,89 @@ export function Home() {
560592
left: repoDropdownPosition.left,
561593
}}
562594
>
563-
{availableModes.map((option) => (
564-
<button
565-
key={option.value}
566-
onClick={() => {
567-
handleRepoModeChange(repo.name, option.value);
568-
setOpenRepoDropdown(null);
569-
}}
570-
className={cn(
571-
"w-full flex items-start gap-2.5 px-3 py-2 hover:bg-muted/50 transition-colors text-left",
572-
repo.mode === option.value && "bg-muted/50"
573-
)}
574-
>
575-
<option.icon className="w-3.5 h-3.5 mt-0.5 shrink-0" />
576-
<div className="flex-1 min-w-0">
577-
<div className="font-medium text-xs">
578-
{option.label}
595+
{showAuthoredByInput === repo.name ? (
596+
<div className="p-3">
597+
<div className="text-xs font-medium mb-2">Enter GitHub username</div>
598+
<form
599+
onSubmit={(e) => {
600+
e.preventDefault();
601+
if (authoredByInput.trim()) {
602+
handleRepoModeChange(repo.name, "authored-by", authoredByInput.trim());
603+
setOpenRepoDropdown(null);
604+
setShowAuthoredByInput(null);
605+
setAuthoredByInput("");
606+
}
607+
}}
608+
>
609+
<div className="flex gap-2">
610+
<div className="relative flex-1">
611+
<span className="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground text-xs">@</span>
612+
<input
613+
type="text"
614+
value={authoredByInput}
615+
onChange={(e) => setAuthoredByInput(e.target.value)}
616+
placeholder="username"
617+
className="w-full h-7 pl-6 pr-2 rounded-md border border-border bg-muted/50 text-xs placeholder:text-muted-foreground/60 focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent"
618+
autoFocus
619+
/>
620+
</div>
621+
<button
622+
type="submit"
623+
disabled={!authoredByInput.trim()}
624+
className="px-2 py-1 rounded-md bg-primary text-primary-foreground text-xs font-medium disabled:opacity-50 disabled:cursor-not-allowed"
625+
>
626+
Apply
627+
</button>
579628
</div>
580-
<div className="text-[10px] text-muted-foreground">
581-
{option.description}
629+
</form>
630+
<button
631+
onClick={() => {
632+
setShowAuthoredByInput(null);
633+
setAuthoredByInput("");
634+
}}
635+
className="mt-2 text-[10px] text-muted-foreground hover:text-foreground transition-colors"
636+
>
637+
← Back to modes
638+
</button>
639+
</div>
640+
) : (
641+
availableModes.map((option) => (
642+
<button
643+
key={option.value}
644+
onClick={() => {
645+
if (option.value === "authored-by") {
646+
setShowAuthoredByInput(repo.name);
647+
setAuthoredByInput(repo.authoredBy || "");
648+
} else {
649+
handleRepoModeChange(repo.name, option.value);
650+
setOpenRepoDropdown(null);
651+
}
652+
}}
653+
className={cn(
654+
"w-full flex items-start gap-2.5 px-3 py-2 hover:bg-muted/50 transition-colors text-left",
655+
repo.mode === option.value && "bg-muted/50"
656+
)}
657+
>
658+
<option.icon className="w-3.5 h-3.5 mt-0.5 shrink-0" />
659+
<div className="flex-1 min-w-0">
660+
<div className="font-medium text-xs">
661+
{option.label}
662+
{option.value === "authored-by" && repo.mode === "authored-by" && repo.authoredBy && (
663+
<span className="text-muted-foreground font-normal ml-1">
664+
@{repo.authoredBy}
665+
</span>
666+
)}
667+
</div>
668+
<div className="text-[10px] text-muted-foreground">
669+
{option.description}
670+
</div>
582671
</div>
583-
</div>
584-
{repo.mode === option.value && (
585-
<Check className="w-3.5 h-3.5 text-primary mt-0.5" />
586-
)}
587-
</button>
588-
))}
672+
{repo.mode === option.value && (
673+
<Check className="w-3.5 h-3.5 text-primary mt-0.5" />
674+
)}
675+
</button>
676+
))
677+
)}
589678
</div>
590679
</>
591680
)}

0 commit comments

Comments
 (0)