Skip to content

Commit 00b42c8

Browse files
authored
feat(notifications): adds toast and drawer notification system (#11)
* feat(notifications): expands error store to notification store with severity and read state * feat(notifications): adds ToastContainer with auto-dismiss * feat(notifications): adds NotificationDrawer slide-out panel * fix(notifications): moves mutedSources export to ToastContainer, removes circular import * feat(notifications): adds bell icon with unread badge to header * refactor(notifications): replaces clear-and-repush with reconciliation * refactor(notifications): migrates callers to appropriate severity * refactor(notifications): removes ErrorBannerList for toast/drawer * fix(notifications): prune maps, hoist constant, memo, focus * fix(notifications): fixes dismiss timeout leak, double-dismiss guard, and adds aria-modal * fix(notifications): clears notification state on logout, removes orphaned toasts on dismiss * fix(notifications): moves mutedSources to errors.ts, adds aria attrs, fixes responsive width, caches matchMedia, clears state on logout * fix(notifications): addresses PR review findings - wraps mutedSources in createSignal for proper SolidJS reactivity - swaps markAllAsRead/setDrawerOpen order so unread highlights render - removes dead errors prop from tab components and DashboardStore - adds type="button" to all notification drawer and toast buttons - adds createMemo for sorted notifications, classList consistency - adds 8 new tests: muting, pruning, aria-expanded, reset, cycling * fix(notifications): adds type=button to logout, mocks matchMedia
1 parent d490ec4 commit 00b42c8

24 files changed

+1568
-341
lines changed

src/app/components/dashboard/ActionsTab.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { createMemo, For, Show } from "solid-js";
22
import { createStore } from "solid-js/store";
3-
import type { WorkflowRun, ApiError } from "../../services/api";
3+
import type { WorkflowRun } from "../../services/api";
44
import { config } from "../../stores/config";
55
import { viewState, setViewState, setTabFilter, resetTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, type ActionsFilterField } from "../../stores/view";
66
import WorkflowRunRow from "./WorkflowRunRow";
77
import IgnoreBadge from "./IgnoreBadge";
8-
import ErrorBannerList from "../shared/ErrorBannerList";
98
import SkeletonRows from "../shared/SkeletonRows";
109
import FilterChips from "../shared/FilterChips";
1110
import type { FilterChipGroupDef } from "../shared/FilterChips";
@@ -14,7 +13,6 @@ import ChevronIcon from "../shared/ChevronIcon";
1413
interface ActionsTabProps {
1514
workflowRuns: WorkflowRun[];
1615
loading?: boolean;
17-
errors?: ApiError[];
1816
}
1917

2018
interface WorkflowGroup {
@@ -199,13 +197,10 @@ export default function ActionsTab(props: ActionsTabProps) {
199197
<SkeletonRows label="Loading workflow runs" />
200198
</Show>
201199

202-
{/* Error */}
203-
<ErrorBannerList errors={props.errors?.map((e) => ({ source: e.repo, message: e.message, retryable: e.retryable }))} />
204-
205200
{/* Empty */}
206201
<Show
207202
when={
208-
!props.loading && (!props.errors || props.errors.length === 0) && repoGroups().length === 0
203+
!props.loading && repoGroups().length === 0
209204
}
210205
>
211206
<div class="p-8 text-center text-gray-500 dark:text-gray-400">

src/app/components/dashboard/DashboardPage.tsx

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@ import IssuesTab from "./IssuesTab";
88
import PullRequestsTab from "./PullRequestsTab";
99
import { config } from "../../stores/config";
1010
import { viewState, updateViewState } from "../../stores/view";
11-
import type { Issue, PullRequest, WorkflowRun, ApiError } from "../../services/api";
11+
import type { Issue, PullRequest, WorkflowRun } from "../../services/api";
1212
import { createPollCoordinator, fetchAllData, type DashboardData } from "../../services/poll";
1313
import { clearAuth, user, onAuthCleared, DASHBOARD_STORAGE_KEY } from "../../stores/auth";
14-
import { getErrors, dismissError } from "../../lib/errors";
15-
import ErrorBannerList from "../shared/ErrorBannerList";
1614

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

@@ -22,7 +20,6 @@ interface DashboardStore {
2220
issues: Issue[];
2321
pullRequests: PullRequest[];
2422
workflowRuns: WorkflowRun[];
25-
errors: ApiError[];
2623
loading: boolean;
2724
lastRefreshedAt: Date | null;
2825
}
@@ -31,7 +28,6 @@ const initialDashboardState: DashboardStore = {
3128
issues: [],
3229
pullRequests: [],
3330
workflowRuns: [],
34-
errors: [],
3531
loading: true,
3632
lastRefreshedAt: null,
3733
};
@@ -51,7 +47,6 @@ function loadCachedDashboard(): DashboardStore {
5147
issues: parsed.issues as Issue[],
5248
pullRequests: parsed.pullRequests as PullRequest[],
5349
workflowRuns: parsed.workflowRuns as WorkflowRun[],
54-
errors: [],
5550
loading: false,
5651
lastRefreshedAt: typeof parsed.lastRefreshedAt === "string" ? new Date(parsed.lastRefreshedAt) : null,
5752
};
@@ -93,7 +88,6 @@ async function pollFetch(): Promise<DashboardData> {
9388
issues: data.issues,
9489
pullRequests: data.pullRequests,
9590
workflowRuns: data.workflowRuns,
96-
errors: data.errors,
9791
loading: false,
9892
lastRefreshedAt: now,
9993
});
@@ -191,35 +185,26 @@ export default function DashboardPage() {
191185
onRefresh={() => _coordinator()?.manualRefresh()}
192186
/>
193187

194-
{/* Global error banner */}
195-
<ErrorBannerList
196-
errors={getErrors().map((e) => ({ source: e.source, message: e.message, retryable: e.retryable }))}
197-
onDismiss={(index) => dismissError(getErrors()[index].id)}
198-
/>
199-
200188
<main class="flex-1 overflow-auto">
201189
<Switch>
202190
<Match when={activeTab() === "issues"}>
203191
<IssuesTab
204192
issues={dashboardData.issues}
205193
loading={dashboardData.loading}
206-
errors={dashboardData.errors}
207194
userLogin={userLogin()}
208195
/>
209196
</Match>
210197
<Match when={activeTab() === "pullRequests"}>
211198
<PullRequestsTab
212199
pullRequests={dashboardData.pullRequests}
213200
loading={dashboardData.loading}
214-
errors={dashboardData.errors}
215201
userLogin={userLogin()}
216202
/>
217203
</Match>
218204
<Match when={activeTab() === "actions"}>
219205
<ActionsTab
220206
workflowRuns={dashboardData.workflowRuns}
221207
loading={dashboardData.loading}
222-
errors={dashboardData.errors}
223208
/>
224209
</Match>
225210
</Switch>

src/app/components/dashboard/IssuesTab.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
22
import { createStore } from "solid-js/store";
33
import { config } from "../../stores/config";
44
import { viewState, setSortPreference, setTabFilter, resetTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, type IssueFilterField } from "../../stores/view";
5-
import type { Issue, ApiError } from "../../services/api";
5+
import type { Issue } from "../../services/api";
66
import ItemRow from "./ItemRow";
77
import IgnoreBadge from "./IgnoreBadge";
88
import SortIcon from "../shared/SortIcon";
9-
import ErrorBannerList from "../shared/ErrorBannerList";
109
import PaginationControls from "../shared/PaginationControls";
1110
import FilterChips from "../shared/FilterChips";
1211
import type { FilterChipGroupDef } from "../shared/FilterChips";
@@ -19,7 +18,6 @@ import { groupByRepo, computePageLayout, slicePageGroups } from "../../lib/group
1918
export interface IssuesTabProps {
2019
issues: Issue[];
2120
loading?: boolean;
22-
errors?: ApiError[];
2321
userLogin: string;
2422
}
2523

@@ -162,8 +160,6 @@ export default function IssuesTab(props: IssuesTabProps) {
162160

163161
return (
164162
<div class="flex flex-col h-full">
165-
<ErrorBannerList errors={props.errors?.map((e) => ({ source: e.repo, message: e.message, retryable: e.retryable }))} />
166-
167163
{/* Column headers */}
168164
<div
169165
role="rowgroup"

src/app/components/dashboard/PullRequestsTab.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@ import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
22
import { createStore } from "solid-js/store";
33
import { config } from "../../stores/config";
44
import { viewState, setSortPreference, ignoreItem, unignoreItem, setTabFilter, resetTabFilter, resetAllTabFilters, type PullRequestFilterField } from "../../stores/view";
5-
import type { PullRequest, ApiError } from "../../services/api";
5+
import type { PullRequest } from "../../services/api";
66
import { deriveInvolvementRoles, prSizeCategory } from "../../lib/format";
77
import ItemRow from "./ItemRow";
88
import StatusDot from "../shared/StatusDot";
99
import IgnoreBadge from "./IgnoreBadge";
1010
import SortIcon from "../shared/SortIcon";
11-
import ErrorBannerList from "../shared/ErrorBannerList";
1211
import PaginationControls from "../shared/PaginationControls";
1312
import FilterChips from "../shared/FilterChips";
1413
import type { FilterChipGroupDef } from "../shared/FilterChips";
@@ -22,7 +21,6 @@ import { groupByRepo, computePageLayout, slicePageGroups } from "../../lib/group
2221
export interface PullRequestsTabProps {
2322
pullRequests: PullRequest[];
2423
loading?: boolean;
25-
errors?: ApiError[];
2624
userLogin: string;
2725
}
2826

@@ -244,8 +242,6 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
244242

245243
return (
246244
<div class="flex flex-col h-full">
247-
<ErrorBannerList errors={props.errors?.map((e) => ({ source: e.repo, message: e.message, retryable: e.retryable }))} />
248-
249245
{/* Column headers */}
250246
<div
251247
role="rowgroup"

0 commit comments

Comments
 (0)