Skip to content

Commit 85317ce

Browse files
committed
feat: adds upstream repo discovery and multi-user tracking
1 parent 5a54903 commit 85317ce

23 files changed

+2848
-48
lines changed

src/app/components/dashboard/ActionsTab.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import ExpandCollapseButtons from "../shared/ExpandCollapseButtons";
1414
interface ActionsTabProps {
1515
workflowRuns: WorkflowRun[];
1616
loading?: boolean;
17+
hasUpstreamRepos?: boolean;
1718
}
1819

1920
interface WorkflowGroup {
@@ -230,6 +231,13 @@ export default function ActionsTab(props: ActionsTabProps) {
230231
</div>
231232
</Show>
232233

234+
{/* Upstream repos exclusion note */}
235+
<Show when={props.hasUpstreamRepos}>
236+
<p class="text-xs text-base-content/40 text-center py-2">
237+
Workflow runs are not tracked for upstream repositories.
238+
</p>
239+
</Show>
240+
233241
{/* Repo groups */}
234242
<Show when={repoGroups().length > 0}>
235243
<For each={repoGroups()}>

src/app/components/dashboard/DashboardPage.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import FilterBar from "../layout/FilterBar";
66
import ActionsTab from "./ActionsTab";
77
import IssuesTab from "./IssuesTab";
88
import PullRequestsTab from "./PullRequestsTab";
9-
import { config, setConfig } from "../../stores/config";
9+
import { config, setConfig, type TrackedUser } from "../../stores/config";
1010
import { viewState, updateViewState } from "../../stores/view";
1111
import type { Issue, PullRequest, WorkflowRun } from "../../services/api";
1212
import { fetchOrgs } from "../../services/api";
@@ -157,6 +157,7 @@ async function pollFetch(): Promise<DashboardData> {
157157
pr.totalReviewCount = e.totalReviewCount;
158158
pr.enriched = e.enriched;
159159
pr.nodeId = e.nodeId;
160+
pr.surfacedBy = e.surfacedBy;
160161
}
161162
} else {
162163
state.pullRequests = data.pullRequests;
@@ -306,6 +307,10 @@ export default function DashboardPage() {
306307
}));
307308

308309
const userLogin = createMemo(() => user()?.login ?? "");
310+
const allUsers = createMemo(() => [
311+
{ login: userLogin(), label: "Me" },
312+
...config.trackedUsers.map((u: TrackedUser) => ({ login: u.login, label: u.login })),
313+
]);
309314

310315
return (
311316
<div class="min-h-screen bg-base-200">
@@ -334,19 +339,24 @@ export default function DashboardPage() {
334339
issues={dashboardData.issues}
335340
loading={dashboardData.loading}
336341
userLogin={userLogin()}
342+
allUsers={allUsers()}
343+
trackedUsers={config.trackedUsers}
337344
/>
338345
</Match>
339346
<Match when={activeTab() === "pullRequests"}>
340347
<PullRequestsTab
341348
pullRequests={dashboardData.pullRequests}
342349
loading={dashboardData.loading}
343350
userLogin={userLogin()}
351+
allUsers={allUsers()}
352+
trackedUsers={config.trackedUsers}
344353
/>
345354
</Match>
346355
<Match when={activeTab() === "actions"}>
347356
<ActionsTab
348357
workflowRuns={dashboardData.workflowRuns}
349358
loading={dashboardData.loading}
359+
hasUpstreamRepos={config.upstreamRepos.length > 0}
350360
/>
351361
</Match>
352362
</Switch>

src/app/components/dashboard/IssuesTab.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { config } from "../../stores/config";
33
import { viewState, setSortPreference, setTabFilter, resetTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, type IssueFilterField } from "../../stores/view";
44
import type { Issue } from "../../services/api";
55
import ItemRow from "./ItemRow";
6+
import UserAvatarBadge from "../shared/UserAvatarBadge";
67
import IgnoreBadge from "./IgnoreBadge";
78
import SortDropdown from "../shared/SortDropdown";
89
import type { SortOption } from "../shared/SortDropdown";
@@ -20,6 +21,8 @@ export interface IssuesTabProps {
2021
issues: Issue[];
2122
loading?: boolean;
2223
userLogin: string;
24+
allUsers?: { login: string; label: string }[];
25+
trackedUsers?: { login: string; avatarUrl: string; name: string | null }[];
2326
}
2427

2528
type SortField = "repo" | "title" | "author" | "createdAt" | "updatedAt" | "comments";
@@ -55,6 +58,19 @@ const sortOptions: SortOption[] = [
5558
export default function IssuesTab(props: IssuesTabProps) {
5659
const [page, setPage] = createSignal(0);
5760

61+
const filterGroups = createMemo<FilterChipGroupDef[]>(() => {
62+
const users = props.allUsers;
63+
if (!users || users.length <= 1) return issueFilterGroups;
64+
return [
65+
...issueFilterGroups,
66+
{
67+
label: "User",
68+
field: "user",
69+
options: users.map((u) => ({ value: u.login, label: u.label })),
70+
},
71+
];
72+
});
73+
5874
const sortPref = createMemo(() => {
5975
const pref = viewState.sortPreferences["issues"];
6076
return pref ?? { field: "updatedAt", direction: "desc" as const };
@@ -87,6 +103,11 @@ export default function IssuesTab(props: IssuesTabProps) {
87103
if (tabFilter.comments === "none" && issue.comments > 0) return false;
88104
}
89105

106+
if (tabFilter.user !== "all") {
107+
const surfacedBy = issue.surfacedBy ?? [props.userLogin];
108+
if (!surfacedBy.includes(tabFilter.user)) return false;
109+
}
110+
90111
meta.set(issue.id, { roles });
91112
return true;
92113
});
@@ -172,7 +193,7 @@ export default function IssuesTab(props: IssuesTabProps) {
172193
onChange={handleSort}
173194
/>
174195
<FilterChips
175-
groups={issueFilterGroups}
196+
groups={filterGroups()}
176197
values={viewState.tabFilters.issues}
177198
onChange={(field, value) => {
178199
setTabFilter("issues", field as IssueFilterField, value);
@@ -291,6 +312,17 @@ export default function IssuesTab(props: IssuesTabProps) {
291312
onIgnore={() => handleIgnore(issue)}
292313
density={config.viewDensity}
293314
commentCount={issue.comments}
315+
surfacedByBadge={
316+
props.trackedUsers && props.trackedUsers.length > 0
317+
? <UserAvatarBadge
318+
users={(issue.surfacedBy ?? []).flatMap((login) => {
319+
const u = props.trackedUsers!.find((t) => t.login === login);
320+
return u ? [{ login: u.login, avatarUrl: u.avatarUrl }] : [];
321+
})}
322+
currentUserLogin={props.userLogin}
323+
/>
324+
: undefined
325+
}
294326
>
295327
<RoleBadge roles={issueMeta().get(issue.id)?.roles ?? []} />
296328
</ItemRow>

src/app/components/dashboard/ItemRow.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface ItemRowProps {
1515
density: "compact" | "comfortable";
1616
commentCount?: number;
1717
hideRepo?: boolean;
18+
surfacedByBadge?: JSX.Element;
1819
}
1920

2021
export default function ItemRow(props: ItemRowProps) {
@@ -94,6 +95,9 @@ export default function ItemRow(props: ItemRowProps) {
9495
{/* Author + time + comment count */}
9596
<div class={`shrink-0 flex flex-col items-end gap-0.5 text-xs text-base-content/60 ${isCompact() ? "" : "pt-0.5"}`}>
9697
<span>{props.author}</span>
98+
<Show when={props.surfacedByBadge !== undefined}>
99+
<div class="relative z-10">{props.surfacedByBadge}</div>
100+
</Show>
97101
<span title={props.createdAt}>{relativeTime(props.createdAt)}</span>
98102
<Show when={(props.commentCount ?? 0) > 0}>
99103
<span class="flex items-center gap-0.5">

src/app/components/dashboard/PullRequestsTab.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { PullRequest } from "../../services/api";
55
import { deriveInvolvementRoles, prSizeCategory } from "../../lib/format";
66
import ExpandCollapseButtons from "../shared/ExpandCollapseButtons";
77
import ItemRow from "./ItemRow";
8+
import UserAvatarBadge from "../shared/UserAvatarBadge";
89
import StatusDot from "../shared/StatusDot";
910
import IgnoreBadge from "./IgnoreBadge";
1011
import SortDropdown from "../shared/SortDropdown";
@@ -23,6 +24,8 @@ export interface PullRequestsTabProps {
2324
pullRequests: PullRequest[];
2425
loading?: boolean;
2526
userLogin: string;
27+
allUsers?: { login: string; label: string }[];
28+
trackedUsers?: { login: string; avatarUrl: string; name: string | null }[];
2629
}
2730

2831
type SortField = "repo" | "title" | "author" | "createdAt" | "updatedAt" | "checkStatus" | "reviewDecision" | "size";
@@ -120,6 +123,19 @@ const sortOptions: SortOption[] = [
120123
export default function PullRequestsTab(props: PullRequestsTabProps) {
121124
const [page, setPage] = createSignal(0);
122125

126+
const filterGroups = createMemo<FilterChipGroupDef[]>(() => {
127+
const users = props.allUsers;
128+
if (!users || users.length <= 1) return prFilterGroups;
129+
return [
130+
...prFilterGroups,
131+
{
132+
label: "User",
133+
field: "user",
134+
options: users.map((u) => ({ value: u.login, label: u.label })),
135+
},
136+
];
137+
});
138+
123139
const sortPref = createMemo(() => {
124140
const pref = viewState.sortPreferences["pullRequests"];
125141
return pref ?? { field: "updatedAt", direction: "desc" as const };
@@ -170,6 +186,11 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
170186
if (sizeCategory !== tabFilters.sizeCategory) return false;
171187
}
172188

189+
if (tabFilters.user !== "all") {
190+
const surfacedBy = pr.surfacedBy ?? [props.userLogin];
191+
if (!surfacedBy.includes(tabFilters.user)) return false;
192+
}
193+
173194
meta.set(pr.id, { roles, sizeCategory });
174195
return true;
175196
});
@@ -261,7 +282,7 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
261282
onChange={handleSort}
262283
/>
263284
<FilterChips
264-
groups={prFilterGroups}
285+
groups={filterGroups()}
265286
values={viewState.tabFilters.pullRequests}
266287
onChange={(field, value) => {
267288
setTabFilter("pullRequests", field as PullRequestFilterField, value);
@@ -435,6 +456,17 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
435456
commentCount={pr.enriched !== false ? pr.comments + pr.reviewThreads : undefined}
436457
onIgnore={() => handleIgnore(pr)}
437458
density={config.viewDensity}
459+
surfacedByBadge={
460+
props.trackedUsers && props.trackedUsers.length > 0
461+
? <UserAvatarBadge
462+
users={(pr.surfacedBy ?? []).flatMap((login) => {
463+
const u = props.trackedUsers!.find((t) => t.login === login);
464+
return u ? [{ login: u.login, avatarUrl: u.avatarUrl }] : [];
465+
})}
466+
currentUserLogin={props.userLogin}
467+
/>
468+
: undefined
469+
}
438470
>
439471
<div class="flex items-center gap-2 flex-wrap">
440472
<Show when={pr.enriched !== false}>

src/app/components/onboarding/OnboardingWizard.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export default function OnboardingWizard() {
1616
const [selectedRepos, setSelectedRepos] = createSignal<RepoRef[]>(
1717
config.selectedRepos.length > 0 ? [...config.selectedRepos] : []
1818
);
19+
const [upstreamRepos, setUpstreamRepos] = createSignal<RepoRef[]>(
20+
config.upstreamRepos.length > 0 ? [...config.upstreamRepos] : []
21+
);
1922

2023
const [loading, setLoading] = createSignal(true);
2124
const [error, setError] = createSignal<string | null>(null);
@@ -53,6 +56,7 @@ export default function OnboardingWizard() {
5356
updateConfig({
5457
selectedOrgs: uniqueOrgs,
5558
selectedRepos: selectedRepos(),
59+
upstreamRepos: upstreamRepos(),
5660
onboardingComplete: true,
5761
});
5862
// Flush synchronously — the debounced persistence effect won't fire before page unload
@@ -108,6 +112,9 @@ export default function OnboardingWizard() {
108112
orgEntries={orgEntries()}
109113
selected={selectedRepos()}
110114
onChange={setSelectedRepos}
115+
showUpstreamDiscovery={true}
116+
upstreamRepos={upstreamRepos()}
117+
onUpstreamChange={setUpstreamRepos}
111118
/>
112119
</Match>
113120
</Switch>

0 commit comments

Comments
 (0)