Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
08322e1
refactor(filters): extracts FilterChipGroupDef and scopeFilterGroup t…
wgordon17 Apr 7, 2026
4816e6d
feat(filters): adds ScopeToggle component for scope filter
wgordon17 Apr 7, 2026
8c6a4e4
feat(filters): adds FilterPopover component with Kobalte Popover
wgordon17 Apr 7, 2026
27c2bb7
feat(filters): adds FilterToolbar composite component with tests
wgordon17 Apr 7, 2026
fd9475c
refactor(filters): migrates IssuesTab from FilterChips to FilterToolbar
wgordon17 Apr 7, 2026
1f2b84f
refactor(filters): migrates PullRequestsTab to FilterToolbar
wgordon17 Apr 7, 2026
abf75b2
refactor(filters): migrates ActionsTab from FilterChips to FilterToolbar
wgordon17 Apr 7, 2026
d0259ca
chore(filters): removes dead resetTabFilter and tabFilterDefaults
wgordon17 Apr 7, 2026
3ec4753
fix(filters): review findings — a11y, memos, defensive defaults
wgordon17 Apr 7, 2026
660244c
fix(tests): updates stale FilterChips reference in ActionsTab test
wgordon17 Apr 7, 2026
45c8c4a
refactor(filters): converts value() to createMemo in FilterPopover
wgordon17 Apr 7, 2026
bea6e10
fix(tests): updates stale chip references in test descriptions
wgordon17 Apr 7, 2026
9ba389d
style(filters): adds font-medium to selected popover options
wgordon17 Apr 7, 2026
4d65883
fix: updates stale chip comments in tab source files
wgordon17 Apr 7, 2026
3f7bbdd
feat(filters): adds Tooltip to ScopeToggle and Reset all button
wgordon17 Apr 7, 2026
806df07
fix(filters): addresses remaining review findings
wgordon17 Apr 7, 2026
8776359
refactor(sort): makes sort preference global instead of per-tab
wgordon17 Apr 7, 2026
4cd3f66
fix(filters): addresses PR review findings
wgordon17 Apr 7, 2026
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
22 changes: 12 additions & 10 deletions src/app/components/dashboard/ActionsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { createEffect, createMemo, For, Show } from "solid-js";
import { createStore } from "solid-js/store";
import type { WorkflowRun } from "../../services/api";
import { config } from "../../stores/config";
import { viewState, setViewState, setTabFilter, resetTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, pruneLockedRepos, type ActionsFilterField } from "../../stores/view";
import { viewState, setViewState, setTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, pruneLockedRepos, type ActionsFilterField } from "../../stores/view";
import WorkflowSummaryCard from "./WorkflowSummaryCard";
import IgnoreBadge from "./IgnoreBadge";
import SkeletonRows from "../shared/SkeletonRows";
import FilterChips from "../shared/FilterChips";
import type { FilterChipGroupDef } from "../shared/FilterChips";
import type { FilterChipGroupDef } from "../shared/filterTypes";
import FilterToolbar from "../shared/FilterToolbar";
import ChevronIcon from "../shared/ChevronIcon";
import ExpandCollapseButtons from "../shared/ExpandCollapseButtons";
import RepoLockControls from "../shared/RepoLockControls";
Expand Down Expand Up @@ -135,6 +135,10 @@ export default function ActionsTab(props: ActionsTabProps) {
[...new Set(props.workflowRuns.map((r) => r.repoFullName))]
);

const ignoredWorkflowRuns = createMemo(() =>
viewState.ignoredItems.filter(i => i.type === "workflowRun")
);

createEffect(() => {
const names = activeRepoNames();
if (names.length === 0) return;
Expand Down Expand Up @@ -163,8 +167,7 @@ export default function ActionsTab(props: ActionsTabProps) {
const filteredRuns = createMemo(() => {
const { org, repo } = viewState.globalFilter;
const ignoredIds = new Set(
viewState.ignoredItems
.filter((i) => i.type === "workflowRun")
ignoredWorkflowRuns()
.map((i) => i.id)
);
const conclusionFilter = viewState.tabFilters.actions.conclusion;
Expand Down Expand Up @@ -211,7 +214,7 @@ export default function ActionsTab(props: ActionsTabProps) {
const highlightedReposActions = createReorderHighlight(
() => repoGroups().map(g => g.repoFullName),
() => viewState.lockedRepos.actions,
() => viewState.ignoredItems.filter(i => i.type === "workflowRun").length,
() => ignoredWorkflowRuns().length,
() => JSON.stringify(viewState.tabFilters.actions),
);

Expand All @@ -229,11 +232,10 @@ export default function ActionsTab(props: ActionsTabProps) {
/>
Show PR runs
</label>
<FilterChips
<FilterToolbar
groups={actionsFilterGroups}
values={viewState.tabFilters.actions}
onChange={(field, value) => setTabFilter("actions", field as ActionsFilterField, value)}
onReset={(field) => resetTabFilter("actions", field as ActionsFilterField)}
onChange={(f, v) => setTabFilter("actions", f as ActionsFilterField, v)}
onResetAll={() => resetAllTabFilters("actions")}
/>
</div>
Expand All @@ -243,7 +245,7 @@ export default function ActionsTab(props: ActionsTabProps) {
onCollapseAll={() => setAllExpanded("actions", repoGroups().map((g) => g.repoFullName), false)}
/>
<IgnoreBadge
items={viewState.ignoredItems.filter((i) => i.type === "workflowRun")}
items={ignoredWorkflowRuns()}
onUnignore={unignoreItem}
/>
</div>
Expand Down
19 changes: 18 additions & 1 deletion src/app/components/dashboard/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import IssuesTab from "./IssuesTab";
import PullRequestsTab from "./PullRequestsTab";
import PersonalSummaryStrip from "./PersonalSummaryStrip";
import { config, setConfig, type TrackedUser } from "../../stores/config";
import { viewState, updateViewState } from "../../stores/view";
import { viewState, updateViewState, setSortPreference } from "../../stores/view";
import type { SortOption } from "../shared/SortDropdown";
import type { Issue, PullRequest, WorkflowRun } from "../../services/api";
import { fetchOrgs } from "../../services/api";
import {
Expand All @@ -27,6 +28,18 @@ import { formatCount } from "../../lib/format";
import { setsEqual } from "../../lib/collections";
import { Tooltip } from "../shared/Tooltip";

const globalSortOptions: SortOption[] = [
{ label: "Repo", field: "repo", type: "text" },
{ label: "Title", field: "title", type: "text" },
{ label: "Author", field: "author", type: "text" },
{ label: "Comments", field: "comments", type: "number" },
{ label: "Checks", field: "checkStatus", type: "text" },
{ label: "Review", field: "reviewDecision", type: "text" },
{ label: "Size", field: "size", type: "number" },
{ label: "Created", field: "createdAt", type: "date" },
{ label: "Updated", field: "updatedAt", type: "date" },
];

// ── Shared dashboard store (module-level to survive navigation) ─────────────

// Bump only for breaking schema changes (renames, type changes). Additive optional
Expand Down Expand Up @@ -397,6 +410,10 @@ export default function DashboardPage() {
isRefreshing={_coordinator()?.isRefreshing() ?? dashboardData.loading}
lastRefreshedAt={_coordinator()?.lastRefreshAt() ?? dashboardData.lastRefreshedAt}
onRefresh={() => _coordinator()?.manualRefresh()}
sortOptions={globalSortOptions}
sortValue={viewState.globalSort.field}
sortDirection={viewState.globalSort.direction}
onSortChange={(field, dir) => setSortPreference(field, dir)}
/>
</div>

Expand Down
23 changes: 15 additions & 8 deletions src/app/components/dashboard/IgnoreBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,21 @@ export default function IgnoreBadge(props: IgnoreBadgeProps) {
return (
<Show when={props.items.length > 0}>
<div class="relative">
<button
onClick={() => setOpen((v) => !v)}
class="badge badge-neutral badge-sm cursor-pointer"
aria-haspopup="true"
aria-expanded={open()}
>
{props.items.length} ignored
</button>
<Tooltip content={`${props.items.length} ignored item${props.items.length === 1 ? "" : "s"}`}>
<button
onClick={() => setOpen((v) => !v)}
class="btn btn-ghost btn-sm relative"
aria-haspopup="true"
aria-expanded={open()}
aria-label={`${props.items.length} ignored items`}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M3.707 2.293a1 1 0 00-1.414 1.414l14 14a1 1 0 001.414-1.414l-1.473-1.473A10.014 10.014 0 0019.542 10C18.268 5.943 14.478 3 10 3a9.958 9.958 0 00-4.512 1.074l-1.78-1.781zm4.261 4.26l1.514 1.515a2.003 2.003 0 012.45 2.45l1.514 1.514a4 4 0 00-5.478-5.478z" clip-rule="evenodd" />
<path d="M12.454 16.697L9.75 13.992a4 4 0 01-3.742-3.741L2.335 6.578A9.98 9.98 0 00.458 10c1.274 4.057 5.065 7 9.542 7 .847 0 1.669-.105 2.454-.303z" />
</svg>
<span class="badge badge-neutral badge-xs absolute -top-1 -right-1">{props.items.length}</span>
</button>
</Tooltip>

<Show when={open()}>
{/* Backdrop */}
Expand Down
66 changes: 24 additions & 42 deletions src/app/components/dashboard/IssuesTab.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
import { config, type TrackedUser } from "../../stores/config";
import { viewState, updateViewState, setSortPreference, setTabFilter, resetTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, pruneLockedRepos, type IssueFilterField } from "../../stores/view";
import { viewState, updateViewState, setTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, pruneLockedRepos, type IssueFilterField } from "../../stores/view";
import type { Issue, RepoRef } 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";
import PaginationControls from "../shared/PaginationControls";
import FilterChips, { scopeFilterGroup, type FilterChipGroupDef } from "../shared/FilterChips";
import { scopeFilterGroup, type FilterChipGroupDef } from "../shared/filterTypes";
import FilterToolbar from "../shared/FilterToolbar";
import RoleBadge from "../shared/RoleBadge";
import SkeletonRows from "../shared/SkeletonRows";
import ChevronIcon from "../shared/ChevronIcon";
Expand Down Expand Up @@ -51,14 +50,6 @@ const issueFilterGroups: FilterChipGroupDef[] = [
},
];

const sortOptions: SortOption[] = [
{ label: "Repo", field: "repo", type: "text" },
{ label: "Title", field: "title", type: "text" },
{ label: "Author", field: "author", type: "text" },
{ label: "Comments", field: "comments", type: "number" },
{ label: "Created", field: "createdAt", type: "date" },
{ label: "Updated", field: "updatedAt", type: "date" },
];

export default function IssuesTab(props: IssuesTabProps) {
const [page, setPage] = createSignal(0);
Expand All @@ -81,6 +72,10 @@ export default function IssuesTab(props: IssuesTabProps) {
(props.monitoredRepos ?? []).length > 0 || (props.allUsers?.length ?? 0) > 1
);

const ignoredIssues = createMemo(() =>
viewState.ignoredItems.filter(i => i.type === "issue")
);

const filterGroups = createMemo<FilterChipGroupDef[]>(() => {
const users = props.allUsers;
const base = showScopeFilter()
Expand All @@ -97,27 +92,29 @@ export default function IssuesTab(props: IssuesTabProps) {
];
});

// Auto-reset scope to default when scope chip is hidden (localStorage hygiene)
// Auto-reset scope to default when scope toggle is hidden (localStorage hygiene)
createEffect(() => {
if (!showScopeFilter() && viewState.tabFilters.issues.scope !== "involves_me") {
setTabFilter("issues", "scope", "involves_me");
}
});

// Auto-reset user filter when User filter group is hidden
createEffect(() => {
const users = props.allUsers;
if ((!users || users.length <= 1) && viewState.tabFilters.issues.user !== "all") {
setTabFilter("issues", "user", "all");
}
});

const isInvolvedItem = (item: Issue) =>
isUserInvolved(item, userLoginLower(), monitoredRepoNameSet());

const sortPref = createMemo(() => {
const pref = viewState.sortPreferences["issues"];
return pref ?? { field: "updatedAt", direction: "desc" as const };
});

const filteredSortedWithMeta = createMemo(() => {
const filter = viewState.globalFilter;
const tabFilter = viewState.tabFilters.issues;
const ignored = new Set(
viewState.ignoredItems
.filter((i) => i.type === "issue")
ignoredIssues()
.map((i) => i.id)
);

Expand Down Expand Up @@ -160,7 +157,7 @@ export default function IssuesTab(props: IssuesTabProps) {
return true;
});

const { field, direction } = sortPref();
const { field, direction } = viewState.globalSort;
items = [...items].sort((a, b) => {
let cmp = 0;
switch (field as SortField) {
Expand Down Expand Up @@ -226,15 +223,10 @@ export default function IssuesTab(props: IssuesTabProps) {
const highlightedReposIssues = createReorderHighlight(
() => repoGroups().map(g => g.repoFullName),
() => viewState.lockedRepos.issues,
() => viewState.ignoredItems.filter(i => i.type === "issue").length,
() => ignoredIssues().length,
() => JSON.stringify(viewState.tabFilters.issues),
);

function handleSort(field: string, direction: "asc" | "desc") {
setSortPreference("issues", field, direction);
setPage(0);
}

function handleIgnore(issue: Issue) {
ignoreItem({
id: String(issue.id),
Expand All @@ -247,24 +239,14 @@ export default function IssuesTab(props: IssuesTabProps) {

return (
<div class="flex flex-col h-full">
{/* Sort dropdown + filter chips + ignore badge toolbar */}
{/* Filter chips + ignore badge toolbar */}
<div class="flex items-start gap-3 px-4 py-2 border-b border-base-300 bg-base-100">
<div class="flex flex-wrap items-center gap-3 min-w-0 flex-1">
<SortDropdown
options={sortOptions}
value={sortPref().field}
direction={sortPref().direction}
onChange={handleSort}
/>
<FilterChips
<FilterToolbar
groups={filterGroups()}
values={viewState.tabFilters.issues}
onChange={(field, value) => {
setTabFilter("issues", field as IssueFilterField, value);
setPage(0);
}}
onReset={(field) => {
resetTabFilter("issues", field as IssueFilterField);
onChange={(f, v) => {
setTabFilter("issues", f as IssueFilterField, v);
setPage(0);
}}
onResetAll={() => {
Expand All @@ -291,7 +273,7 @@ export default function IssuesTab(props: IssuesTabProps) {
onCollapseAll={() => setAllExpanded("issues", repoGroups().map((g) => g.repoFullName), false)}
/>
<IgnoreBadge
items={viewState.ignoredItems.filter((i) => i.type === "issue")}
items={ignoredIssues()}
onUnignore={unignoreItem}
/>
</div>
Expand Down
40 changes: 18 additions & 22 deletions src/app/components/dashboard/PersonalSummaryStrip.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { createMemo, For, Show } from "solid-js";
import { produce } from "solid-js/store";
import type { Issue, PullRequest, WorkflowRun } from "../../services/api";
import type { TabId } from "../layout/TabBar";
import { viewState, updateViewState, resetAllTabFilters, setTabFilter } from "../../stores/view";
import { viewState, setViewState, IssueFiltersSchema, PullRequestFiltersSchema, ActionsFiltersSchema } from "../../stores/view";
import { InfoTooltip } from "../shared/Tooltip";

interface SummaryCount {
Expand Down Expand Up @@ -104,55 +105,50 @@ export default function PersonalSummaryStrip(props: PersonalSummaryStripProps) {
count: assignedIssues,
tab: "issues",
applyFilters: () => {
resetAllTabFilters("issues");
setTabFilter("issues", "scope", "all");
setTabFilter("issues", "role", "assignee");
setViewState(produce(draft => {
draft.tabFilters.issues = { ...IssueFiltersSchema.parse({}), scope: "all", role: "assignee" };
}));
},
});
if (prsAwaitingReview > 0) items.push({
label: prsAwaitingReview === 1 ? "PR awaiting review" : "PRs awaiting review",
count: prsAwaitingReview,
tab: "pullRequests",
applyFilters: () => {
resetAllTabFilters("pullRequests");
setTabFilter("pullRequests", "scope", "all");
setTabFilter("pullRequests", "role", "reviewer");
setTabFilter("pullRequests", "reviewDecision", "REVIEW_REQUIRED");
setViewState(produce(draft => {
draft.tabFilters.pullRequests = { ...PullRequestFiltersSchema.parse({}), scope: "all", role: "reviewer", reviewDecision: "REVIEW_REQUIRED" };
}));
},
});
if (prsReadyToMerge > 0) items.push({
label: prsReadyToMerge === 1 ? "PR ready to merge" : "PRs ready to merge",
count: prsReadyToMerge,
tab: "pullRequests",
applyFilters: () => {
resetAllTabFilters("pullRequests");
setTabFilter("pullRequests", "scope", "all");
setTabFilter("pullRequests", "role", "author");
setTabFilter("pullRequests", "draft", "ready");
setTabFilter("pullRequests", "checkStatus", "success");
setTabFilter("pullRequests", "reviewDecision", "mergeable");
setViewState(produce(draft => {
draft.tabFilters.pullRequests = { ...PullRequestFiltersSchema.parse({}), scope: "all", role: "author", draft: "ready", checkStatus: "success", reviewDecision: "mergeable" };
}));
},
});
if (prsBlocked > 0) items.push({
label: prsBlocked === 1 ? "PR blocked" : "PRs blocked",
count: prsBlocked,
tab: "pullRequests",
applyFilters: () => {
resetAllTabFilters("pullRequests");
setTabFilter("pullRequests", "scope", "all");
setTabFilter("pullRequests", "role", "author");
setTabFilter("pullRequests", "draft", "ready");
setTabFilter("pullRequests", "checkStatus", "blocked");
setViewState(produce(draft => {
draft.tabFilters.pullRequests = { ...PullRequestFiltersSchema.parse({}), scope: "all", role: "author", draft: "ready", checkStatus: "blocked" };
}));
},
});
if (running > 0) items.push({
label: running === 1 ? "action running" : "actions running",
count: running,
tab: "actions",
applyFilters: () => {
resetAllTabFilters("actions");
setTabFilter("actions", "conclusion", "running");
updateViewState({ showPrRuns: true });
setViewState(produce(draft => {
draft.tabFilters.actions = { ...ActionsFiltersSchema.parse({}), conclusion: "running" };
draft.showPrRuns = true;
}));
},
});
return items;
Expand Down
Loading