Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2d6bd90
feat(tabs): adds custom tab schema and routing
wgordon17 Apr 19, 2026
556845b
feat(tabs): wires custom tab filter integration
wgordon17 Apr 19, 2026
8880f77
feat(tabs): creates CustomTabModal and extracts filter groups
wgordon17 Apr 19, 2026
3463118
feat(settings): adds Custom Tabs CRUD section
wgordon17 Apr 19, 2026
a1a70be
fix(tabs): reinitializes modal form on reopen
wgordon17 Apr 19, 2026
27468ed
fix(tabs): hardens schema validation and accessibility
wgordon17 Apr 19, 2026
95e66c3
docs: documents custom tabs in README and user guide
wgordon17 Apr 19, 2026
05bab53
test: adds CustomTabModal and CustomTabsSection tests
wgordon17 Apr 19, 2026
7fe3c58
fix(tabs): verifies Kobalte keyboard nav is safe with wrapper divs
wgordon17 Apr 19, 2026
6a18854
test: adds exclusivity ownership tests to DashboardPage
wgordon17 Apr 19, 2026
f1321fb
fix(tabs): addresses quality gate review findings
wgordon17 Apr 19, 2026
4e98aee
fix(tabs): closes modal on edited tab deletion, fixes toggle a11y
wgordon17 Apr 19, 2026
036c6c1
fix(settings): replaces window.confirm with inline delete
wgordon17 Apr 19, 2026
b519438
fix(tabs): address pr-review findings
wgordon17 Apr 20, 2026
28e8e2d
fix(tabs): complete tabCounts filter parity and share constants
wgordon17 Apr 20, 2026
c7856da
chore: merges duplicate format import, tightens cast
wgordon17 Apr 20, 2026
df9a50f
fix(tabs): address pr-review findings (cycle 2)
wgordon17 Apr 21, 2026
1105070
fix(tabs): deduplicate tabCounts merge, add enriched guard
wgordon17 Apr 21, 2026
4c5ea7e
fix(tabs): adds tooltips, replaces edit icon
wgordon17 Apr 21, 2026
5f094bd
fix(tabs): matches tooltip patterns, consistent edit icon
wgordon17 Apr 21, 2026
ff37cfc
fix(tabs): uses descriptive tooltip text in settings
wgordon17 Apr 21, 2026
4355cde
chore: merges upstream/main to resolve PR conflicts
wgordon17 Apr 21, 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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ Shimmer animations on items being updated by the hot poll, flash highlights when

Star counts appear in repo group headers, fetched as part of the standard data refresh.

### Custom Tabs

Create named filtered views over the existing Issues, PRs, and Actions data. Each custom tab has a name, a base type (Issues, PRs, or Actions), an optional org/repo scope, and optional filter presets. An "exclusive" toggle hides matching items from the standard tabs so they only appear in the custom tab. Up to 10 custom tabs can be created. Manage them via the "+" button in the tab bar or in **Settings > Custom Tabs**.

### Themes

9 themes: auto (follows system), corporate, cupcake, light, nord, dim, dracula, dark, forest. Theme is applied immediately on selection with no page reload.
Expand Down
19 changes: 18 additions & 1 deletion docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ GitHub Tracker is a dashboard that aggregates open issues, pull requests, and Gi
- [Organization Access](#organization-access)
- [Dashboard Overview](#dashboard-overview)
- [Tab Structure](#tab-structure)
- [Custom Tabs](#custom-tabs)
- [Personal Summary Strip](#personal-summary-strip)
- [Repo Grouping and Expand/Collapse](#repo-grouping-and-expandcollapse)
- [Scope Filter](#scope-filter)
Expand Down Expand Up @@ -73,17 +74,33 @@ OAuth sign-in uses your existing GitHub org memberships. If a private organizati

### Tab Structure

The dashboard has three tabs by default, with an optional fourth:
The dashboard has three built-in tabs by default, with optional additional tabs:

| Tab | Contents |
|-----|----------|
| **Issues** | Open issues across your selected repos where you are the author, assignee, or mentioned |
| **Pull Requests** | Open PRs where you are the author, reviewer, or assignee |
| **Actions** | Recent workflow runs for your selected repos |
| **Tracked** | Manually pinned issues and PRs (opt-in via Settings) |
| **Custom tabs** | Named filtered views you define (up to 10, see [Custom Tabs](#custom-tabs)) |

The active tab is remembered across page loads by default. You can set a fixed default tab in Settings.

### Custom Tabs

Custom tabs let you create named, filtered views over the Issues, PRs, or Actions data. For example, you could create a "My PRs" tab showing only PRs you authored, or a "Needs review" tab scoped to a single org.

**Creating a tab:** Click the **+** button at the right end of the tab bar (desktop) or go to **Settings > Custom Tabs** (mobile or desktop). Each tab requires:

- **Name** — displayed in the tab bar
- **Base type** — Issues, Pull Requests, or Actions
- **Scope** (optional) — restrict to a specific org or repo
- **Filter presets** (optional) — pre-apply one or more filters (e.g., Role: Author, Checks: Failing). Filters use the same options as the corresponding built-in tab. The value `_self` in user-based filters resolves to your authenticated login at runtime.

**Exclusive toggle:** When enabled, items that match the custom tab's scope and filters are hidden from the standard Issues, Pull Requests, or Actions tab. They appear only in the custom tab. Items in the Tracked tab are never hidden by exclusivity.

**Managing tabs:** In **Settings > Custom Tabs** you can edit, reorder, and delete custom tabs. Up to 10 custom tabs are supported.

### Personal Summary Strip

A summary strip appears directly below the tab bar whenever there is actionable activity. It shows counts for:
Expand Down
89 changes: 38 additions & 51 deletions src/app/components/dashboard/ActionsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { createEffect, createMemo, For, Show } from "solid-js";
import { createStore } from "solid-js/store";
import type { WorkflowRun } from "../../services/api";
import { viewState, setViewState, setTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, pruneLockedRepos, type ActionsFilterField } from "../../stores/view";
import { viewState, setViewState, ignoreItem, unignoreItem, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, pruneLockedRepos, ActionsFiltersSchema } from "../../stores/view";
import { createTabFilterHandlers, mergeActiveFilters } from "../../lib/tabFilters";
import { isRunVisible } from "../../lib/filters";
import WorkflowSummaryCard from "./WorkflowSummaryCard";
import IgnoreBadge from "./IgnoreBadge";
import SkeletonRows from "../shared/SkeletonRows";
import type { FilterChipGroupDef } from "../shared/filterTypes";
import { actionsFilterGroups, KNOWN_CONCLUSIONS, KNOWN_EVENTS } from "../shared/filterTypes";
import FilterToolbar from "../shared/FilterToolbar";
import RepoGroupHeader from "../shared/RepoGroupHeader";
import ExpandCollapseButtons from "../shared/ExpandCollapseButtons";
Expand All @@ -23,6 +25,8 @@ interface ActionsTabProps {
configRepoNames?: string[];
refreshTick?: number;
hotPollingRunIds?: ReadonlySet<number>;
customTabId?: string;
filterPreset?: Record<string, string>;
}

interface WorkflowGroup {
Expand Down Expand Up @@ -98,36 +102,14 @@ function sortWorkflowsByStatus(workflows: WorkflowGroup[]): WorkflowGroup[] {
});
}

const KNOWN_CONCLUSIONS = ["success", "failure", "cancelled"];
const KNOWN_EVENTS = ["push", "pull_request", "schedule", "workflow_dispatch"];

const actionsFilterGroups: FilterChipGroupDef[] = [
{
label: "Result",
field: "conclusion",
options: [
{ value: "success", label: "Success" },
{ value: "failure", label: "Failure" },
{ value: "cancelled", label: "Cancelled" },
{ value: "running", label: "Running" },
{ value: "other", label: "Other" },
],
},
{
label: "Trigger",
field: "event",
options: [
{ value: "push", label: "Push" },
{ value: "pull_request", label: "PR" },
{ value: "schedule", label: "Schedule" },
{ value: "workflow_dispatch", label: "Manual" },
],
},
];

const ACTIONS_FILTER_DEFAULTS = ActionsFiltersSchema.parse({});

export default function ActionsTab(props: ActionsTabProps) {
const [expandedWorkflows, setExpandedWorkflows] = createStore<Record<string, boolean>>({});

const tabKey = () => props.customTabId ?? "actions";

function toggleWorkflow(key: string) {
setExpandedWorkflows(key, (v) => !v);
}
Expand All @@ -140,16 +122,25 @@ export default function ActionsTab(props: ActionsTabProps) {
viewState.ignoredItems.filter(i => i.type === "workflowRun")
);

// Merge chain: schema defaults → preset → stored runtime overrides
const activeFilters = createMemo(() =>
mergeActiveFilters(ActionsFiltersSchema, ACTIONS_FILTER_DEFAULTS, props.customTabId, viewState.tabFilters.actions, {
preset: props.filterPreset,
})
);

const { handleFilterChange, handleResetFilters } = createTabFilterHandlers("actions", () => props.customTabId);

createEffect(() => {
const names = activeRepoNames();
if (names.length === 0) return;
pruneExpandedRepos("actions", names);
pruneExpandedRepos(tabKey(), names);
});

const { flashingIds: flashingRunIds, peekUpdates } = createFlashDetection({
getItems: () => props.workflowRuns,
getHotIds: () => props.hotPollingRunIds,
getExpandedRepos: () => viewState.expandedRepos.actions,
getExpandedRepos: () => viewState.expandedRepos[tabKey()] ?? {},
trackKey: (run) => `${run.status}|${run.conclusion}`,
itemLabel: (run) => run.name,
itemStatus: (run) => run.conclusion ?? run.status,
Expand All @@ -166,33 +157,27 @@ export default function ActionsTab(props: ActionsTabProps) {
}

const filteredRuns = createMemo(() => {
const { org, repo } = viewState.globalFilter;
const ignoredIds = new Set(
ignoredWorkflowRuns()
.map((i) => i.id)
);
const conclusionFilter = viewState.tabFilters.actions.conclusion;
const eventFilter = viewState.tabFilters.actions.event;
const ignoredIds = new Set(ignoredWorkflowRuns().map((i) => i.id));
const globalFilter = props.customTabId ? null : viewState.globalFilter;
const conclusionFilter = activeFilters().conclusion;
const eventFilter = activeFilters().event;

return props.workflowRuns.filter((run) => {
if (ignoredIds.has(run.id)) return false;
if (!viewState.showPrRuns && run.isPrRun) return false;
if (org && !run.repoFullName.startsWith(`${org}/`)) return false;
if (repo && run.repoFullName !== repo) return false;
if (!isRunVisible(run, { ignoredIds, showPrRuns: viewState.showPrRuns, globalFilter })) return false;

if (conclusionFilter !== "all") {
if (conclusionFilter === "running") {
if (run.status !== "in_progress") return false;
} else if (conclusionFilter === "other") {
if (run.conclusion === null || KNOWN_CONCLUSIONS.includes(run.conclusion)) return false;
if (run.conclusion === null || (KNOWN_CONCLUSIONS as readonly string[]).includes(run.conclusion)) return false;
} else {
if (run.conclusion !== conclusionFilter) return false;
}
}

if (eventFilter !== "all") {
if (eventFilter === "other") {
if (KNOWN_EVENTS.includes(run.event)) return false;
if ((KNOWN_EVENTS as readonly string[]).includes(run.event)) return false;
} else {
if (run.event !== eventFilter) return false;
}
Expand Down Expand Up @@ -222,7 +207,9 @@ export default function ActionsTab(props: ActionsTabProps) {
() => repoGroups().map(g => g.repoFullName),
() => viewState.lockedRepos,
() => ignoredWorkflowRuns().length,
() => JSON.stringify(viewState.tabFilters.actions),
() => JSON.stringify(props.customTabId
? (viewState.customTabFilters[props.customTabId] ?? {})
: viewState.tabFilters.actions),
);

return (
Expand All @@ -241,15 +228,15 @@ export default function ActionsTab(props: ActionsTabProps) {
</label>
<FilterToolbar
groups={actionsFilterGroups}
values={viewState.tabFilters.actions}
onChange={(f, v) => setTabFilter("actions", f as ActionsFilterField, v)}
onResetAll={() => resetAllTabFilters("actions")}
values={activeFilters()}
onChange={(f, v) => handleFilterChange(f, v)}
onResetAll={() => handleResetFilters()}
/>
</div>
<div class="shrink-0 flex items-center gap-2 py-0.5">
<ExpandCollapseButtons
onExpandAll={() => setAllExpanded("actions", repoGroups().map((g) => g.repoFullName), true)}
onCollapseAll={() => setAllExpanded("actions", repoGroups().map((g) => g.repoFullName), false)}
onExpandAll={() => setAllExpanded(tabKey(), repoGroups().map((g) => g.repoFullName), true)}
onCollapseAll={() => setAllExpanded(tabKey(), repoGroups().map((g) => g.repoFullName), false)}
/>
<IgnoreBadge
items={ignoredWorkflowRuns()}
Expand Down Expand Up @@ -279,7 +266,7 @@ export default function ActionsTab(props: ActionsTabProps) {
<For each={repoGroups()}>
{(repoGroup) => {
const isEmpty = () => repoGroup.workflows.length === 0;
const isExpanded = () => !isEmpty() && !!viewState.expandedRepos.actions[repoGroup.repoFullName];
const isExpanded = () => !isEmpty() && !!(viewState.expandedRepos[tabKey()] ?? {})[repoGroup.repoFullName];

const sortedWorkflows = createMemo(() =>
sortWorkflowsByStatus(repoGroup.workflows)
Expand Down Expand Up @@ -312,7 +299,7 @@ export default function ActionsTab(props: ActionsTabProps) {
repoFullName={repoGroup.repoFullName}
isExpanded={isExpanded()}
isHighlighted={highlightedReposActions().has(repoGroup.repoFullName)}
onToggle={() => toggleExpandedRepo("actions", repoGroup.repoFullName)}
onToggle={() => toggleExpandedRepo(tabKey(), repoGroup.repoFullName)}
trailing={
<>
<RepoGitHubLink repoFullName={repoGroup.repoFullName} section="actions" />
Expand Down
Loading