Skip to content

Commit 5592758

Browse files
committed
refactor(ui): addresses PR review findings across date display PR
- Extracts RepoGitHubLink shared component, replacing duplicated anchor markup in IssuesTab, PullRequestsTab, and ActionsTab - Moves clockInterval inside onMount to match coordinator lifecycle pattern - Promotes depDashboard from string enum in tabFilters to top-level boolean (hideDepDashboard), eliminating resetAllTabFilters preserve hack - Expands void refreshTick comments to explain SolidJS dependency tracking mechanism - Normalizes Date.parse() usage in formatDuration for consistency with relativeTime/shortRelativeTime - Adds tests for mixed valid/invalid dates, 360-day boundary, external link rendering, clock cleanup specificity, and hideDepDashboard lifecycle
1 parent d4ecda3 commit 5592758

File tree

14 files changed

+119
-73
lines changed

14 files changed

+119
-73
lines changed

src/app/components/dashboard/ActionsTab.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { FilterChipGroupDef } from "../shared/FilterChips";
1111
import ChevronIcon from "../shared/ChevronIcon";
1212
import ExpandCollapseButtons from "../shared/ExpandCollapseButtons";
1313
import RepoLockControls from "../shared/RepoLockControls";
14-
import ExternalLinkIcon from "../shared/ExternalLinkIcon";
14+
import RepoGitHubLink from "../shared/RepoGitHubLink";
1515
import { orderRepoGroups } from "../../lib/grouping";
1616
import { createReorderHighlight } from "../../lib/reorderHighlight";
1717
import { createFlashDetection } from "../../lib/flashDetection";
@@ -320,16 +320,7 @@ export default function ActionsTab(props: ActionsTabProps) {
320320
</span>
321321
</Show>
322322
</button>
323-
<a
324-
href={`https://github.com/${repoGroup.repoFullName}/actions`}
325-
target="_blank"
326-
rel="noopener noreferrer"
327-
class="opacity-0 group-hover/repo-header:opacity-100 focus-visible:opacity-100 transition-opacity text-base-content/40 hover:text-primary px-1"
328-
title={`Open ${repoGroup.repoFullName} actions on GitHub`}
329-
aria-label={`Open ${repoGroup.repoFullName} actions on GitHub`}
330-
>
331-
<ExternalLinkIcon />
332-
</a>
323+
<RepoGitHubLink repoFullName={repoGroup.repoFullName} section="actions" />
333324
<RepoLockControls tab="actions" repoFullName={repoGroup.repoFullName} />
334325
</div>
335326
<Show when={!isExpanded() && peekUpdates().get(repoGroup.repoFullName)}>

src/app/components/dashboard/DashboardPage.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ export default function DashboardPage() {
240240
updateViewState({ lastActiveTab: tab });
241241
}
242242

243+
const [clockTick, setClockTick] = createSignal(0);
244+
243245
onMount(() => {
244246
if (!_coordinator()) {
245247
_setCoordinator(createPollCoordinator(() => config.refreshInterval, pollFetch));
@@ -306,20 +308,19 @@ export default function DashboardPage() {
306308
});
307309
}
308310

311+
// Wall-clock tick keeps relative time displays fresh between full poll cycles.
312+
const clockInterval = setInterval(() => setClockTick((t) => t + 1), 60_000);
313+
309314
onCleanup(() => {
310315
_coordinator()?.destroy();
311316
_setCoordinator(null);
312317
_hotCoordinator()?.destroy();
313318
_setHotCoordinator(null);
314319
clearHotSets();
320+
clearInterval(clockInterval);
315321
});
316322
});
317323

318-
// Wall-clock tick keeps relative time displays fresh between full poll cycles.
319-
const [clockTick, setClockTick] = createSignal(0);
320-
const clockInterval = setInterval(() => setClockTick((t) => t + 1), 60_000);
321-
onCleanup(() => clearInterval(clockInterval));
322-
323324
const refreshTick = createMemo(() => (dashboardData.lastRefreshedAt?.getTime() ?? 0) + clockTick());
324325

325326
const tabCounts = createMemo(() => ({

src/app/components/dashboard/IssuesTab.tsx

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
22
import { config, type TrackedUser } from "../../stores/config";
3-
import { viewState, setSortPreference, setTabFilter, resetTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, pruneLockedRepos, type IssueFilterField } from "../../stores/view";
3+
import { viewState, updateViewState, setSortPreference, setTabFilter, resetTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, pruneLockedRepos, type IssueFilterField } from "../../stores/view";
44
import type { Issue, RepoRef } from "../../services/api";
55
import ItemRow from "./ItemRow";
66
import UserAvatarBadge, { buildSurfacedByUsers } from "../shared/UserAvatarBadge";
@@ -18,7 +18,7 @@ import { deriveInvolvementRoles } from "../../lib/format";
1818
import { groupByRepo, computePageLayout, slicePageGroups, orderRepoGroups } from "../../lib/grouping";
1919
import { createReorderHighlight } from "../../lib/reorderHighlight";
2020
import RepoLockControls from "../shared/RepoLockControls";
21-
import ExternalLinkIcon from "../shared/ExternalLinkIcon";
21+
import RepoGitHubLink from "../shared/RepoGitHubLink";
2222

2323
export interface IssuesTabProps {
2424
issues: Issue[];
@@ -120,7 +120,7 @@ export default function IssuesTab(props: IssuesTabProps) {
120120
if (tabFilter.comments === "none" && issue.comments > 0) return false;
121121
}
122122

123-
if (tabFilter.depDashboard === "hide" && issue.title === "Dependency Dashboard") return false;
123+
if (viewState.hideDepDashboard && issue.title === "Dependency Dashboard") return false;
124124

125125
if (tabFilter.user !== "all") {
126126
// Items from monitored repos bypass the surfacedBy filter (all activity is shown)
@@ -248,12 +248,11 @@ export default function IssuesTab(props: IssuesTabProps) {
248248
/>
249249
<button
250250
onClick={() => {
251-
const next = viewState.tabFilters.issues.depDashboard === "show" ? "hide" : "show";
252-
setTabFilter("issues", "depDashboard", next);
251+
updateViewState({ hideDepDashboard: !viewState.hideDepDashboard });
253252
setPage(0);
254253
}}
255-
class={`btn btn-xs rounded-full ${viewState.tabFilters.issues.depDashboard === "show" ? "btn-primary" : "btn-ghost text-base-content/50"}`}
256-
aria-pressed={viewState.tabFilters.issues.depDashboard === "show"}
254+
class={`btn btn-xs rounded-full ${!viewState.hideDepDashboard ? "btn-primary" : "btn-ghost text-base-content/50"}`}
255+
aria-pressed={!viewState.hideDepDashboard}
257256
title="Toggle visibility of Dependency Dashboard issues"
258257
>
259258
Show Dep Dashboard
@@ -349,16 +348,7 @@ export default function IssuesTab(props: IssuesTabProps) {
349348
</span>
350349
</Show>
351350
</button>
352-
<a
353-
href={`https://github.com/${repoGroup.repoFullName}/issues`}
354-
target="_blank"
355-
rel="noopener noreferrer"
356-
class="opacity-0 group-hover/repo-header:opacity-100 focus-visible:opacity-100 transition-opacity text-base-content/40 hover:text-primary px-1"
357-
title={`Open ${repoGroup.repoFullName} issues on GitHub`}
358-
aria-label={`Open ${repoGroup.repoFullName} issues on GitHub`}
359-
>
360-
<ExternalLinkIcon />
361-
</a>
351+
<RepoGitHubLink repoFullName={repoGroup.repoFullName} section="issues" />
362352
<RepoLockControls tab="issues" repoFullName={repoGroup.repoFullName} />
363353
</div>
364354
<Show when={isExpanded()}>

src/app/components/dashboard/ItemRow.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ export default function ItemRow(props: ItemRowProps) {
3535
return { createdTitle, updatedTitle, diffMs };
3636
});
3737

38-
// void refreshTick — Date.now() is not reactive in SolidJS
38+
// Reading props.refreshTick registers it as a SolidJS reactive dependency,
39+
// forcing this memo to re-evaluate when the tick changes. Date.now() alone
40+
// is not tracked by SolidJS's dependency system.
3941
const dateDisplay = createMemo(() => {
4042
void props.refreshTick;
4143
const created = shortRelativeTime(props.createdAt);

src/app/components/dashboard/PullRequestsTab.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { groupByRepo, computePageLayout, slicePageGroups, orderRepoGroups } from
2222
import { createReorderHighlight } from "../../lib/reorderHighlight";
2323
import { createFlashDetection } from "../../lib/flashDetection";
2424
import RepoLockControls from "../shared/RepoLockControls";
25-
import ExternalLinkIcon from "../shared/ExternalLinkIcon";
25+
import RepoGitHubLink from "../shared/RepoGitHubLink";
2626

2727
export interface PullRequestsTabProps {
2828
pullRequests: PullRequest[];
@@ -490,16 +490,7 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
490490
</span>
491491
</Show>
492492
</button>
493-
<a
494-
href={`https://github.com/${repoGroup.repoFullName}/pulls`}
495-
target="_blank"
496-
rel="noopener noreferrer"
497-
class="opacity-0 group-hover/repo-header:opacity-100 focus-visible:opacity-100 transition-opacity text-base-content/40 hover:text-primary px-1"
498-
title={`Open ${repoGroup.repoFullName} pull requests on GitHub`}
499-
aria-label={`Open ${repoGroup.repoFullName} pull requests on GitHub`}
500-
>
501-
<ExternalLinkIcon />
502-
</a>
493+
<RepoGitHubLink repoFullName={repoGroup.repoFullName} section="pulls" />
503494
<RepoLockControls tab="pullRequests" repoFullName={repoGroup.repoFullName} />
504495
</div>
505496
<Show when={!isExpanded() && peekUpdates().get(repoGroup.repoFullName)}>

src/app/components/dashboard/WorkflowRunRow.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,9 @@ export default function WorkflowRunRow(props: WorkflowRunRowProps) {
134134

135135
const createdTitle = createMemo(() => `Created: ${new Date(props.run.createdAt).toLocaleString()}`);
136136

137-
// void refreshTick — Date.now() is not reactive in SolidJS
137+
// Reading props.refreshTick registers it as a SolidJS reactive dependency,
138+
// forcing this memo to re-evaluate when the tick changes. Date.now() alone
139+
// is not tracked by SolidJS's dependency system.
138140
const timeLabel = createMemo(() => {
139141
void props.refreshTick;
140142
return relativeTime(props.run.createdAt);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import ExternalLinkIcon from "./ExternalLinkIcon";
2+
3+
const sectionLabels = {
4+
issues: "issues",
5+
pulls: "pull requests",
6+
actions: "actions",
7+
} as const;
8+
9+
export default function RepoGitHubLink(props: {
10+
repoFullName: string;
11+
section: "issues" | "pulls" | "actions";
12+
}) {
13+
const label = () => sectionLabels[props.section];
14+
15+
return (
16+
<a
17+
href={`https://github.com/${props.repoFullName}/${props.section}`}
18+
target="_blank"
19+
rel="noopener noreferrer"
20+
class="opacity-0 group-hover/repo-header:opacity-100 focus-visible:opacity-100 transition-opacity text-base-content/40 hover:text-primary px-1"
21+
title={`Open ${props.repoFullName} ${label()} on GitHub`}
22+
aria-label={`Open ${props.repoFullName} ${label()} on GitHub`}
23+
>
24+
<ExternalLinkIcon />
25+
</a>
26+
);
27+
}

src/app/lib/format.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function labelTextColor(hexColor: string): string {
6363
export function formatDuration(startedAt: string, completedAt: string | null): string {
6464
if (!startedAt) return "--";
6565
if (!completedAt) return "--";
66-
const diffMs = new Date(completedAt).getTime() - new Date(startedAt).getTime();
66+
const diffMs = Date.parse(completedAt) - Date.parse(startedAt);
6767
if (diffMs <= 0) return "--";
6868
const totalSec = Math.floor(diffMs / 1000);
6969
const h = Math.floor(totalSec / 3600);

src/app/stores/view.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ const IssueFiltersSchema = z.object({
99
role: z.enum(["all", "author", "assignee"]).default("all"),
1010
comments: z.enum(["all", "has", "none"]).default("all"),
1111
user: z.enum(["all"]).or(z.string()).default("all"),
12-
depDashboard: z.enum(["hide", "show"]).default("hide"),
1312
});
1413

1514
const PullRequestFiltersSchema = z.object({
@@ -64,15 +63,16 @@ export const ViewStateSchema = z.object({
6463
})
6564
.default({ org: null, repo: null }),
6665
tabFilters: z.object({
67-
issues: IssueFiltersSchema.default({ role: "all", comments: "all", user: "all", depDashboard: "hide" }),
66+
issues: IssueFiltersSchema.default({ role: "all", comments: "all", user: "all" }),
6867
pullRequests: PullRequestFiltersSchema.default({ role: "all", reviewDecision: "all", draft: "all", checkStatus: "all", sizeCategory: "all", user: "all" }),
6968
actions: ActionsFiltersSchema.default({ conclusion: "all", event: "all" }),
7069
}).default({
71-
issues: { role: "all", comments: "all", user: "all", depDashboard: "hide" },
70+
issues: { role: "all", comments: "all", user: "all" },
7271
pullRequests: { role: "all", reviewDecision: "all", draft: "all", checkStatus: "all", sizeCategory: "all", user: "all" },
7372
actions: { conclusion: "all", event: "all" },
7473
}),
7574
showPrRuns: z.boolean().default(false),
75+
hideDepDashboard: z.boolean().default(true),
7676
expandedRepos: z.object({
7777
issues: z.record(z.string(), z.boolean()).default({}),
7878
pullRequests: z.record(z.string(), z.boolean()).default({}),
@@ -122,11 +122,12 @@ export function resetViewState(): void {
122122
ignoredItems: [],
123123
globalFilter: { org: null, repo: null },
124124
tabFilters: {
125-
issues: { role: "all", comments: "all", user: "all", depDashboard: "hide" },
125+
issues: { role: "all", comments: "all", user: "all" },
126126
pullRequests: { role: "all", reviewDecision: "all", draft: "all", checkStatus: "all", sizeCategory: "all", user: "all" },
127127
actions: { conclusion: "all", event: "all" },
128128
},
129129
showPrRuns: false,
130+
hideDepDashboard: true,
130131
expandedRepos: { issues: {}, pullRequests: {}, actions: {} },
131132
lockedRepos: { issues: [], pullRequests: [], actions: [] },
132133
});
@@ -224,9 +225,7 @@ export function resetAllTabFilters(
224225
setViewState(
225226
produce((draft) => {
226227
if (tab === "issues") {
227-
const depDashboard = draft.tabFilters.issues.depDashboard;
228228
draft.tabFilters.issues = IssueFiltersSchema.parse({});
229-
draft.tabFilters.issues.depDashboard = depDashboard;
230229
} else if (tab === "pullRequests") {
231230
draft.tabFilters.pullRequests = PullRequestFiltersSchema.parse({});
232231
} else {

tests/components/DashboardPage.test.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,14 +188,15 @@ describe("DashboardPage — clock tick", () => {
188188
spy.mockRestore();
189189
});
190190

191-
it("clears the interval on unmount", () => {
191+
it("clears the clock interval on unmount", () => {
192192
const setSpy = vi.spyOn(globalThis, "setInterval");
193193
const clearSpy = vi.spyOn(globalThis, "clearInterval");
194194
const { unmount } = render(() => <DashboardPage />);
195-
const clockCall = setSpy.mock.calls.find(([, ms]) => ms === 60_000);
196-
expect(clockCall).toBeDefined();
195+
const clockCallIdx = setSpy.mock.calls.findIndex(([, ms]) => ms === 60_000);
196+
expect(clockCallIdx).not.toBe(-1);
197+
const clockIntervalId = setSpy.mock.results[clockCallIdx].value;
197198
unmount();
198-
expect(clearSpy).toHaveBeenCalled();
199+
expect(clearSpy).toHaveBeenCalledWith(clockIntervalId);
199200
setSpy.mockRestore();
200201
clearSpy.mockRestore();
201202
});

0 commit comments

Comments
 (0)