From 52b3749a7e4ea07b22d842b1555360acd6078969 Mon Sep 17 00:00:00 2001 From: testvalue Date: Fri, 17 Apr 2026 10:17:06 -0400 Subject: [PATCH 1/6] feat(view): unify lockedRepos and preserve empty-repo state - Flatten lockedRepos from per-tab object to shared z.array(z.string()) - Add migrateLockedRepos() with shape-based detection and lastActiveTab dedup precedence for backward-compatible localStorage migration - Remove tab parameter from lockRepo, unlockRepo, moveLockedRepo, pruneLockedRepos; remove LockedReposTab type - Add configRepoNames memo in DashboardPage (selectedRepos + upstreamRepos + monitoredRepos) for config-derived pruning - Pass configRepoNames to IssuesTab, PullRequestsTab, ActionsTab as optional prop with item-derived fallback - Remove tab prop from RepoLockControls - Update USER_GUIDE pin state docs from per-tab to cross-tab shared - Add 11 new tests: 5 migrateLockedRepos unit tests, 6 empty-repo state preservation tests across 3 dashboard tab test files --- docs/USER_GUIDE.md | 6 +- src/app/components/dashboard/ActionsTab.tsx | 11 +- .../components/dashboard/DashboardPage.tsx | 7 + src/app/components/dashboard/IssuesTab.tsx | 11 +- .../components/dashboard/PullRequestsTab.tsx | 11 +- .../components/shared/RepoLockControls.tsx | 13 +- src/app/stores/view.ts | 60 ++++--- .../components/dashboard/ActionsTab.test.tsx | 35 ++++- tests/components/dashboard/IssuesTab.test.tsx | 36 ++++- .../dashboard/PullRequestsTab.test.tsx | 36 ++++- .../shared/RepoLockControls.test.tsx | 68 ++++---- tests/stores/view-lock.test.ts | 148 ++++++++++-------- 12 files changed, 293 insertions(+), 149 deletions(-) diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 949b4ea6..c48aee6d 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -350,13 +350,13 @@ The Tracked tab lets you pin issues and PRs into a personal TODO list that you c ## Repo Pinning -Each repo group header has a pin (lock) control, visible on hover on desktop and always visible on mobile. Pinning a repo keeps it at the top of the list within its tab regardless of sort order or how recently it was updated. +Each repo group header has a pin (lock) control, visible on hover on desktop and always visible on mobile. Pinning a repo keeps it at the top of the list on all tabs regardless of sort order or how recently it was updated. - Click the pin icon to pin a repo to the top. - Click it again to unpin. - Use the up/down arrows (visible when pinned) to reorder pinned repos relative to each other. -Pin state is per-tab — a repo can be pinned on the Issues tab but not the Pull Requests tab. +Pin state is shared across all tabs — pinning a repo on the Issues tab also pins it on Pull Requests and Actions. --- @@ -438,7 +438,7 @@ These are UI preferences that persist across sessions but are not included in th | Show PR runs (Actions) | Off | Whether to show workflow runs triggered by pull request events. | | Hide Dependency Dashboard | On | Whether to hide the Renovate Dependency Dashboard issue. | | Sort preferences | Updated (desc) | Sort field and direction per tab, remembered across sessions. | -| Pinned repos | (none) | Repos pinned to the top of each tab's list. | +| Pinned repos | (none) | Repos pinned to the top of the list across all tabs. | | Tracked items | (none) | Issues and PRs pinned to the Tracked tab (max 200). | --- diff --git a/src/app/components/dashboard/ActionsTab.tsx b/src/app/components/dashboard/ActionsTab.tsx index a9fe8095..54aeac56 100644 --- a/src/app/components/dashboard/ActionsTab.tsx +++ b/src/app/components/dashboard/ActionsTab.tsx @@ -19,6 +19,7 @@ interface ActionsTabProps { workflowRuns: WorkflowRun[]; loading?: boolean; hasUpstreamRepos?: boolean; + configRepoNames?: string[]; refreshTick?: number; hotPollingRunIds?: ReadonlySet; } @@ -131,7 +132,7 @@ export default function ActionsTab(props: ActionsTabProps) { } const activeRepoNames = createMemo(() => - [...new Set(props.workflowRuns.map((r) => r.repoFullName))] + props.configRepoNames ?? [...new Set(props.workflowRuns.map((r) => r.repoFullName))] ); const ignoredWorkflowRuns = createMemo(() => @@ -201,18 +202,18 @@ export default function ActionsTab(props: ActionsTabProps) { }); const repoGroups = createMemo(() => - orderRepoGroups(groupRuns(filteredRuns()), viewState.lockedRepos.actions) + orderRepoGroups(groupRuns(filteredRuns()), viewState.lockedRepos) ); createEffect(() => { const names = activeRepoNames(); if (names.length === 0) return; - pruneLockedRepos("actions", names); + pruneLockedRepos(names); }); const highlightedReposActions = createReorderHighlight( () => repoGroups().map(g => g.repoFullName), - () => viewState.lockedRepos.actions, + () => viewState.lockedRepos, () => ignoredWorkflowRuns().length, () => JSON.stringify(viewState.tabFilters.actions), ); @@ -327,7 +328,7 @@ export default function ActionsTab(props: ActionsTabProps) { - + {(peek) => ( diff --git a/src/app/components/dashboard/DashboardPage.tsx b/src/app/components/dashboard/DashboardPage.tsx index 84d11fc4..140d562f 100644 --- a/src/app/components/dashboard/DashboardPage.tsx +++ b/src/app/components/dashboard/DashboardPage.tsx @@ -507,6 +507,10 @@ export default function DashboardPage() { ]; }); + const configRepoNames = createMemo(() => + [...new Set([...config.selectedRepos, ...config.upstreamRepos, ...config.monitoredRepos].map(r => r.fullName))] + ); + return (
@@ -550,6 +554,7 @@ export default function DashboardPage() { allUsers={allUsers()} trackedUsers={config.trackedUsers} monitoredRepos={config.monitoredRepos} + configRepoNames={configRepoNames()} refreshTick={refreshTick()} /> @@ -562,6 +567,7 @@ export default function DashboardPage() { trackedUsers={config.trackedUsers} hotPollingPRIds={hotPollingPRIds()} monitoredRepos={config.monitoredRepos} + configRepoNames={configRepoNames()} refreshTick={refreshTick()} /> @@ -579,6 +585,7 @@ export default function DashboardPage() { workflowRuns={dashboardData.workflowRuns} loading={dashboardData.loading} hasUpstreamRepos={config.upstreamRepos.length > 0} + configRepoNames={configRepoNames()} refreshTick={refreshTick()} hotPollingRunIds={hotPollingRunIds()} /> diff --git a/src/app/components/dashboard/IssuesTab.tsx b/src/app/components/dashboard/IssuesTab.tsx index ef7e9a4e..bef78a88 100644 --- a/src/app/components/dashboard/IssuesTab.tsx +++ b/src/app/components/dashboard/IssuesTab.tsx @@ -26,6 +26,7 @@ export interface IssuesTabProps { allUsers?: { login: string; label: string }[]; trackedUsers?: TrackedUser[]; monitoredRepos?: RepoRef[]; + configRepoNames?: string[]; refreshTick?: number; } @@ -191,7 +192,7 @@ export default function IssuesTab(props: IssuesTabProps) { const issueMeta = createMemo(() => filteredSortedWithMeta().meta); const repoGroups = createMemo(() => - orderRepoGroups(groupByRepo(filteredSorted()), viewState.lockedRepos.issues) + orderRepoGroups(groupByRepo(filteredSorted()), viewState.lockedRepos) ); const pageLayout = createMemo(() => computePageLayout(repoGroups(), config.itemsPerPage)); const pageCount = createMemo(() => pageLayout().pageCount); @@ -205,7 +206,7 @@ export default function IssuesTab(props: IssuesTabProps) { }); const activeRepoNames = createMemo(() => - [...new Set(props.issues.map((i) => i.repoFullName))] + props.configRepoNames ?? [...new Set(props.issues.map((i) => i.repoFullName))] ); createEffect(() => { @@ -217,7 +218,7 @@ export default function IssuesTab(props: IssuesTabProps) { createEffect(() => { const names = activeRepoNames(); if (names.length === 0) return; - pruneLockedRepos("issues", names); + pruneLockedRepos(names); }); const trackedIssueIds = createMemo(() => @@ -228,7 +229,7 @@ export default function IssuesTab(props: IssuesTabProps) { const highlightedReposIssues = createReorderHighlight( () => repoGroups().map(g => g.repoFullName), - () => viewState.lockedRepos.issues, + () => viewState.lockedRepos, () => ignoredIssues().length, () => JSON.stringify(viewState.tabFilters.issues), ); @@ -386,7 +387,7 @@ export default function IssuesTab(props: IssuesTabProps) { - +
diff --git a/src/app/components/dashboard/PullRequestsTab.tsx b/src/app/components/dashboard/PullRequestsTab.tsx index 83bb07a6..105721f1 100644 --- a/src/app/components/dashboard/PullRequestsTab.tsx +++ b/src/app/components/dashboard/PullRequestsTab.tsx @@ -32,6 +32,7 @@ export interface PullRequestsTabProps { trackedUsers?: TrackedUser[]; hotPollingPRIds?: ReadonlySet; monitoredRepos?: RepoRef[]; + configRepoNames?: string[]; refreshTick?: number; } @@ -288,7 +289,7 @@ export default function PullRequestsTab(props: PullRequestsTabProps) { const prMeta = createMemo(() => filteredSortedWithMeta().meta); const repoGroups = createMemo(() => - orderRepoGroups(groupByRepo(filteredSorted()), viewState.lockedRepos.pullRequests) + orderRepoGroups(groupByRepo(filteredSorted()), viewState.lockedRepos) ); const pageLayout = createMemo(() => computePageLayout(repoGroups(), config.itemsPerPage)); const pageCount = createMemo(() => pageLayout().pageCount); @@ -302,7 +303,7 @@ export default function PullRequestsTab(props: PullRequestsTabProps) { }); const activeRepoNames = createMemo(() => - [...new Set(props.pullRequests.map((pr) => pr.repoFullName))] + props.configRepoNames ?? [...new Set(props.pullRequests.map((pr) => pr.repoFullName))] ); createEffect(() => { @@ -314,7 +315,7 @@ export default function PullRequestsTab(props: PullRequestsTabProps) { createEffect(() => { const names = activeRepoNames(); if (names.length === 0) return; - pruneLockedRepos("pullRequests", names); + pruneLockedRepos(names); }); const { flashingIds: flashingPRIds, peekUpdates } = createFlashDetection({ @@ -334,7 +335,7 @@ export default function PullRequestsTab(props: PullRequestsTabProps) { const highlightedReposPRs = createReorderHighlight( () => repoGroups().map(g => g.repoFullName), - () => viewState.lockedRepos.pullRequests, + () => viewState.lockedRepos, () => ignoredPullRequests().length, () => JSON.stringify(viewState.tabFilters.pullRequests), ); @@ -535,7 +536,7 @@ export default function PullRequestsTab(props: PullRequestsTabProps) { - +
{(peek) => ( diff --git a/src/app/components/shared/RepoLockControls.tsx b/src/app/components/shared/RepoLockControls.tsx index aea82998..be6cbf25 100644 --- a/src/app/components/shared/RepoLockControls.tsx +++ b/src/app/components/shared/RepoLockControls.tsx @@ -1,16 +1,15 @@ import { Show, createMemo } from "solid-js"; -import { viewState, lockRepo, unlockRepo, moveLockedRepo, type LockedReposTab } from "../../stores/view"; +import { viewState, lockRepo, unlockRepo, moveLockedRepo } from "../../stores/view"; import { Tooltip } from "./Tooltip"; import { withFlipAnimation } from "../../lib/scroll"; interface RepoLockControlsProps { - tab: LockedReposTab; repoFullName: string; } export default function RepoLockControls(props: RepoLockControlsProps) { const lockInfo = createMemo(() => { - const list = viewState.lockedRepos[props.tab]; + const list = viewState.lockedRepos; const idx = list.indexOf(props.repoFullName); return { isLocked: idx !== -1, @@ -27,7 +26,7 @@ export default function RepoLockControls(props: RepoLockControlsProps) {