Skip to content
Merged
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
8 changes: 8 additions & 0 deletions src/app/components/dashboard/ActionsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import ExpandCollapseButtons from "../shared/ExpandCollapseButtons";
interface ActionsTabProps {
workflowRuns: WorkflowRun[];
loading?: boolean;
hasUpstreamRepos?: boolean;
}

interface WorkflowGroup {
Expand Down Expand Up @@ -319,6 +320,13 @@ export default function ActionsTab(props: ActionsTabProps) {
}}
</For>
</Show>

{/* Upstream repos exclusion note */}
<Show when={props.hasUpstreamRepos}>
<p class="text-xs text-base-content/40 text-center py-2">
Workflow runs are not tracked for upstream repositories.
</p>
</Show>
</div>
);
}
16 changes: 15 additions & 1 deletion src/app/components/dashboard/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import FilterBar from "../layout/FilterBar";
import ActionsTab from "./ActionsTab";
import IssuesTab from "./IssuesTab";
import PullRequestsTab from "./PullRequestsTab";
import { config, setConfig } from "../../stores/config";
import { config, setConfig, type TrackedUser } from "../../stores/config";
import { viewState, updateViewState } from "../../stores/view";
import type { Issue, PullRequest, WorkflowRun } from "../../services/api";
import { fetchOrgs } from "../../services/api";
Expand Down Expand Up @@ -158,6 +158,7 @@ async function pollFetch(): Promise<DashboardData> {
pr.totalReviewCount = e.totalReviewCount;
pr.enriched = e.enriched;
pr.nodeId = e.nodeId;
pr.surfacedBy = e.surfacedBy;
}
} else {
state.pullRequests = data.pullRequests;
Expand Down Expand Up @@ -307,6 +308,14 @@ export default function DashboardPage() {
}));

const userLogin = createMemo(() => user()?.login ?? "");
const allUsers = createMemo(() => {
const login = userLogin().toLowerCase();
if (!login) return [];
return [
{ login, label: "Me" },
...config.trackedUsers.map((u: TrackedUser) => ({ login: u.login, label: u.login })),
];
});

return (
<div class="min-h-screen bg-base-200">
Expand Down Expand Up @@ -335,19 +344,24 @@ export default function DashboardPage() {
issues={dashboardData.issues}
loading={dashboardData.loading}
userLogin={userLogin()}
allUsers={allUsers()}
trackedUsers={config.trackedUsers}
/>
</Match>
<Match when={activeTab() === "pullRequests"}>
<PullRequestsTab
pullRequests={dashboardData.pullRequests}
loading={dashboardData.loading}
userLogin={userLogin()}
allUsers={allUsers()}
trackedUsers={config.trackedUsers}
/>
</Match>
<Match when={activeTab() === "actions"}>
<ActionsTab
workflowRuns={dashboardData.workflowRuns}
loading={dashboardData.loading}
hasUpstreamRepos={config.upstreamRepos.length > 0}
/>
</Match>
</Switch>
Expand Down
46 changes: 43 additions & 3 deletions src/app/components/dashboard/IssuesTab.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
import { config } from "../../stores/config";
import { config, type TrackedUser } from "../../stores/config";
import { viewState, setSortPreference, setTabFilter, resetTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, type IssueFilterField } from "../../stores/view";
import type { Issue } from "../../services/api";
import ItemRow from "./ItemRow";
import UserAvatarBadge, { buildSurfacedByUsers } from "../shared/UserAvatarBadge";
import IgnoreBadge from "./IgnoreBadge";
import SortDropdown from "../shared/SortDropdown";
import type { SortOption } from "../shared/SortDropdown";
Expand All @@ -20,6 +21,8 @@ export interface IssuesTabProps {
issues: Issue[];
loading?: boolean;
userLogin: string;
allUsers?: { login: string; label: string }[];
trackedUsers?: TrackedUser[];
}

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

const trackedUserMap = createMemo(() =>
new Map(props.trackedUsers?.map(u => [u.login, u]) ?? [])
);

const upstreamRepoSet = createMemo(() =>
new Set((config.upstreamRepos ?? []).map(r => r.fullName))
);

const filterGroups = createMemo<FilterChipGroupDef[]>(() => {
const users = props.allUsers;
if (!users || users.length <= 1) return issueFilterGroups;
return [
...issueFilterGroups,
{
label: "User",
field: "user",
options: users.map((u) => ({ value: u.login, label: u.label })),
},
];
});

const sortPref = createMemo(() => {
const pref = viewState.sortPreferences["issues"];
return pref ?? { field: "updatedAt", direction: "desc" as const };
Expand All @@ -76,7 +100,7 @@ export default function IssuesTab(props: IssuesTabProps) {
if (filter.repo && issue.repoFullName !== filter.repo) return false;
if (filter.org && !issue.repoFullName.startsWith(filter.org + "/")) return false;

const roles = deriveInvolvementRoles(props.userLogin, issue.userLogin, issue.assigneeLogins, []);
const roles = deriveInvolvementRoles(props.userLogin, issue.userLogin, issue.assigneeLogins, [], upstreamRepoSet().has(issue.repoFullName));

if (tabFilter.role !== "all") {
if (!roles.includes(tabFilter.role as "author" | "assignee")) return false;
Expand All @@ -87,6 +111,14 @@ export default function IssuesTab(props: IssuesTabProps) {
if (tabFilter.comments === "none" && issue.comments > 0) return false;
}

if (tabFilter.user !== "all") {
const validUser = !props.allUsers || props.allUsers.some(u => u.login === tabFilter.user);
if (validUser) {
const surfacedBy = issue.surfacedBy ?? [props.userLogin.toLowerCase()];
if (!surfacedBy.includes(tabFilter.user)) return false;
}
}

meta.set(issue.id, { roles });
return true;
});
Expand Down Expand Up @@ -172,7 +204,7 @@ export default function IssuesTab(props: IssuesTabProps) {
onChange={handleSort}
/>
<FilterChips
groups={issueFilterGroups}
groups={filterGroups()}
values={viewState.tabFilters.issues}
onChange={(field, value) => {
setTabFilter("issues", field as IssueFilterField, value);
Expand Down Expand Up @@ -291,6 +323,14 @@ export default function IssuesTab(props: IssuesTabProps) {
onIgnore={() => handleIgnore(issue)}
density={config.viewDensity}
commentCount={issue.comments}
surfacedByBadge={
props.trackedUsers && props.trackedUsers.length > 0
? <UserAvatarBadge
users={buildSurfacedByUsers(issue.surfacedBy, trackedUserMap())}
currentUserLogin={props.userLogin}
/>
: undefined
}
>
<RoleBadge roles={issueMeta().get(issue.id)?.roles ?? []} />
</ItemRow>
Expand Down
7 changes: 6 additions & 1 deletion src/app/components/dashboard/ItemRow.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { For, JSX, Show } from "solid-js";
import { isSafeGitHubUrl } from "../../lib/url";
import { relativeTime, labelTextColor, formatCount } from "../../lib/format";
import { expandEmoji } from "../../lib/emoji";

export interface ItemRowProps {
repo: string;
Expand All @@ -15,6 +16,7 @@ export interface ItemRowProps {
density: "compact" | "comfortable";
commentCount?: number;
hideRepo?: boolean;
surfacedByBadge?: JSX.Element;
}

export default function ItemRow(props: ItemRowProps) {
Expand Down Expand Up @@ -77,7 +79,7 @@ export default function ItemRow(props: ItemRowProps) {
class="inline-flex items-center rounded-full text-xs px-2 py-0.5 font-medium bg-[var(--lb)] text-[var(--lf)]"
style={{ "--lb": bg, "--lf": fg }}
>
{label.name}
{expandEmoji(label.name)}
</span>
);
}}
Expand All @@ -94,6 +96,9 @@ export default function ItemRow(props: ItemRowProps) {
{/* Author + time + comment count */}
<div class={`shrink-0 flex flex-col items-end gap-0.5 text-xs text-base-content/60 ${isCompact() ? "" : "pt-0.5"}`}>
<span>{props.author}</span>
<Show when={props.surfacedByBadge !== undefined}>
<div class="relative z-10">{props.surfacedByBadge}</div>
</Show>
<span title={props.createdAt}>{relativeTime(props.createdAt)}</span>
<Show when={(props.commentCount ?? 0) > 0}>
<span class="flex items-center gap-0.5">
Expand Down
46 changes: 43 additions & 3 deletions src/app/components/dashboard/PullRequestsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
import { config } from "../../stores/config";
import { config, type TrackedUser } from "../../stores/config";
import { viewState, setSortPreference, ignoreItem, unignoreItem, setTabFilter, resetTabFilter, resetAllTabFilters, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, type PullRequestFilterField } from "../../stores/view";
import type { PullRequest } from "../../services/api";
import { deriveInvolvementRoles, prSizeCategory } from "../../lib/format";
import ExpandCollapseButtons from "../shared/ExpandCollapseButtons";
import ItemRow from "./ItemRow";
import UserAvatarBadge, { buildSurfacedByUsers } from "../shared/UserAvatarBadge";
import StatusDot from "../shared/StatusDot";
import IgnoreBadge from "./IgnoreBadge";
import SortDropdown from "../shared/SortDropdown";
Expand All @@ -23,6 +24,8 @@ export interface PullRequestsTabProps {
pullRequests: PullRequest[];
loading?: boolean;
userLogin: string;
allUsers?: { login: string; label: string }[];
trackedUsers?: TrackedUser[];
}

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

const trackedUserMap = createMemo(() =>
new Map(props.trackedUsers?.map(u => [u.login, u]) ?? [])
);

const upstreamRepoSet = createMemo(() =>
new Set((config.upstreamRepos ?? []).map(r => r.fullName))
);

const filterGroups = createMemo<FilterChipGroupDef[]>(() => {
const users = props.allUsers;
if (!users || users.length <= 1) return prFilterGroups;
return [
...prFilterGroups,
{
label: "User",
field: "user",
options: users.map((u) => ({ value: u.login, label: u.label })),
},
];
});

const sortPref = createMemo(() => {
const pref = viewState.sortPreferences["pullRequests"];
return pref ?? { field: "updatedAt", direction: "desc" as const };
Expand All @@ -141,7 +165,7 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
if (filter.repo && pr.repoFullName !== filter.repo) return false;
if (filter.org && !pr.repoFullName.startsWith(filter.org + "/")) return false;

const roles = deriveInvolvementRoles(props.userLogin, pr.userLogin, pr.assigneeLogins, pr.reviewerLogins);
const roles = deriveInvolvementRoles(props.userLogin, pr.userLogin, pr.assigneeLogins, pr.reviewerLogins, upstreamRepoSet().has(pr.repoFullName));
const sizeCategory = prSizeCategory(pr.additions, pr.deletions);

// Tab filters — light-field filters always apply; heavy-field filters
Expand Down Expand Up @@ -170,6 +194,14 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
if (sizeCategory !== tabFilters.sizeCategory) return false;
}

if (tabFilters.user !== "all") {
const validUser = !props.allUsers || props.allUsers.some(u => u.login === tabFilters.user);
if (validUser) {
const surfacedBy = pr.surfacedBy ?? [props.userLogin.toLowerCase()];
if (!surfacedBy.includes(tabFilters.user)) return false;
}
}

meta.set(pr.id, { roles, sizeCategory });
return true;
});
Expand Down Expand Up @@ -261,7 +293,7 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
onChange={handleSort}
/>
<FilterChips
groups={prFilterGroups}
groups={filterGroups()}
values={viewState.tabFilters.pullRequests}
onChange={(field, value) => {
setTabFilter("pullRequests", field as PullRequestFilterField, value);
Expand Down Expand Up @@ -435,6 +467,14 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
commentCount={pr.enriched !== false ? pr.comments + pr.reviewThreads : undefined}
onIgnore={() => handleIgnore(pr)}
density={config.viewDensity}
surfacedByBadge={
props.trackedUsers && props.trackedUsers.length > 0
? <UserAvatarBadge
users={buildSurfacedByUsers(pr.surfacedBy, trackedUserMap())}
currentUserLogin={props.userLogin}
/>
: undefined
}
>
<div class="flex items-center gap-2 flex-wrap">
<Show when={pr.enriched !== false}>
Expand Down
14 changes: 11 additions & 3 deletions src/app/components/onboarding/OnboardingWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export default function OnboardingWizard() {
const [selectedRepos, setSelectedRepos] = createSignal<RepoRef[]>(
config.selectedRepos.length > 0 ? [...config.selectedRepos] : []
);
const [upstreamRepos, setUpstreamRepos] = createSignal<RepoRef[]>(
config.upstreamRepos.length > 0 ? [...config.upstreamRepos] : []
);

const [loading, setLoading] = createSignal(true);
const [error, setError] = createSignal<string | null>(null);
Expand Down Expand Up @@ -53,6 +56,7 @@ export default function OnboardingWizard() {
updateConfig({
selectedOrgs: uniqueOrgs,
selectedRepos: selectedRepos(),
upstreamRepos: upstreamRepos(),
onboardingComplete: true,
});
// Flush synchronously — the debounced persistence effect won't fire before page unload
Expand Down Expand Up @@ -108,6 +112,10 @@ export default function OnboardingWizard() {
orgEntries={orgEntries()}
selected={selectedRepos()}
onChange={setSelectedRepos}
showUpstreamDiscovery={true}
upstreamRepos={upstreamRepos()}
onUpstreamChange={setUpstreamRepos}
trackedUsers={config.trackedUsers}
/>
</Match>
</Switch>
Expand All @@ -120,12 +128,12 @@ export default function OnboardingWizard() {
<button
type="button"
onClick={handleFinish}
disabled={selectedRepos().length === 0}
disabled={selectedRepos().length === 0 && upstreamRepos().length === 0}
class="btn btn-primary"
>
{selectedRepos().length === 0
{selectedRepos().length + upstreamRepos().length === 0
? "Finish Setup"
: `Finish Setup (${selectedRepos().length} ${selectedRepos().length === 1 ? "repo" : "repos"})`}
: `Finish Setup (${selectedRepos().length + upstreamRepos().length} ${selectedRepos().length + upstreamRepos().length === 1 ? "repo" : "repos"})`}
</button>
</div>
</Show>
Expand Down
Loading
Loading