Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 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
c4660f1
Merge remote-tracking branch 'upstream/main' into feat/per-tab-locks
wgordon17 Apr 22, 2026
425bc2f
feat(tabs): per-tab lockedRepos with independent lock lists per tab
wgordon17 Apr 22, 2026
ca7c69a
fix(view): defensive array copy in migrateLockedRepos
wgordon17 Apr 22, 2026
fb5c264
test(locks): add reference independence assertion for migration
wgordon17 Apr 22, 2026
5bd037f
fix(view): filters non-string elements in cap guard, adds coverage tests
wgordon17 Apr 22, 2026
c326fc0
refactor(view): pr-review quality fixes
wgordon17 Apr 23, 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
6 changes: 3 additions & 3 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,13 +367,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 on all tabs regardless of sort order or how recently it was updated. Pinned repos remain visible even when filters exclude all their items — they appear as compact, de-emphasized rows with the repo name and pin controls still accessible.
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 regardless of sort order or how recently it was updated. Pinned repos remain visible even when filters exclude all their items — they appear as compact, de-emphasized rows with the repo name and pin controls still accessible.

- 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 shared across all tabs — pinning a repo on the Issues tab also pins it on Pull Requests and Actions.
Pin state is per-tab — pinning a repo on the Issues tab does not pin it on Pull Requests or Actions. Each tab maintains its own independent pin list and ordering.

---

Expand Down Expand Up @@ -455,7 +455,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 the list across all tabs. |
| Pinned repos | (none) | Repos pinned to the top of the list, stored per tab independently. |
| Tracked items | (none) | Issues and PRs pinned to the Tracked tab (max 200). |

---
Expand Down
13 changes: 7 additions & 6 deletions src/app/components/dashboard/ActionsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,23 +189,24 @@ export default function ActionsTab(props: ActionsTabProps) {

const repoGroups = createMemo(() => {
const groups = groupRuns(filteredRuns());
const lockedForTab = viewState.lockedRepos[tabKey()] ?? [];
const withLocked = ensureLockedRepoGroups(
groups,
viewState.lockedRepos,
lockedForTab,
(name) => ({ repoFullName: name, workflows: [] }),
);
return orderRepoGroups(withLocked, viewState.lockedRepos);
return orderRepoGroups(withLocked, lockedForTab);
});

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

const highlightedReposActions = createReorderHighlight(
() => repoGroups().map(g => g.repoFullName),
() => viewState.lockedRepos,
() => viewState.lockedRepos[tabKey()] ?? [],
() => ignoredWorkflowRuns().length,
() => JSON.stringify(props.customTabId
? (viewState.customTabFilters[props.customTabId] ?? {})
Expand Down Expand Up @@ -291,7 +292,7 @@ export default function ActionsTab(props: ActionsTabProps) {
<Show
when={!isEmpty()}
fallback={
<EmptyLockedRepoRow repoFullName={repoGroup.repoFullName} section="actions" />
<EmptyLockedRepoRow repoFullName={repoGroup.repoFullName} section="actions" tabKey={tabKey()} />
}
>
<div class="bg-base-100" data-repo-group={repoGroup.repoFullName}>
Expand All @@ -303,7 +304,7 @@ export default function ActionsTab(props: ActionsTabProps) {
trailing={
<>
<RepoGitHubLink repoFullName={repoGroup.repoFullName} section="actions" />
<RepoLockControls repoFullName={repoGroup.repoFullName} />
<RepoLockControls repoFullName={repoGroup.repoFullName} tabKey={tabKey()} />
</>
}
collapsedSummary={
Expand Down
1 change: 1 addition & 0 deletions src/app/components/dashboard/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,7 @@ export default function DashboardPage() {
const keys = new Set([
...Object.keys(viewState.customTabFilters),
...Object.keys(viewState.expandedRepos).filter((k) => !isBuiltinTab(k)),
...Object.keys(viewState.lockedRepos).filter((k) => !isBuiltinTab(k)),
]);
return [...keys].filter((id) => !activeIds.has(id));
});
Expand Down
13 changes: 7 additions & 6 deletions src/app/components/dashboard/IssuesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,13 @@ export default function IssuesTab(props: IssuesTabProps) {

const repoGroups = createMemo(() => {
const groups = groupByRepo(filteredSorted());
const lockedForTab = viewState.lockedRepos[tabKey()] ?? [];
const withLocked = ensureLockedRepoGroups(
groups,
viewState.lockedRepos,
lockedForTab,
(name) => ({ repoFullName: name, items: [] as typeof groups[0]["items"] }),
);
return orderRepoGroups(withLocked, viewState.lockedRepos);
return orderRepoGroups(withLocked, lockedForTab);
});
const pageLayout = createMemo(() => computePageLayout(repoGroups(), config.itemsPerPage));
const pageCount = createMemo(() => pageLayout().pageCount);
Expand All @@ -218,7 +219,7 @@ export default function IssuesTab(props: IssuesTabProps) {
createEffect(() => {
const names = activeRepoNames();
if (names.length === 0) return;
pruneLockedRepos(names);
pruneLockedRepos(tabKey(), names);
});

const trackedIssueIds = createMemo(() =>
Expand All @@ -229,7 +230,7 @@ export default function IssuesTab(props: IssuesTabProps) {

const highlightedReposIssues = createReorderHighlight(
() => repoGroups().map(g => g.repoFullName),
() => viewState.lockedRepos,
() => viewState.lockedRepos[tabKey()] ?? [],
() => ignoredIssues().length,
() => JSON.stringify(props.customTabId
? (viewState.customTabFilters[props.customTabId] ?? {})
Expand Down Expand Up @@ -355,7 +356,7 @@ export default function IssuesTab(props: IssuesTabProps) {
<Show
when={!isEmpty()}
fallback={
<EmptyLockedRepoRow repoFullName={repoGroup.repoFullName} section="issues" />
<EmptyLockedRepoRow repoFullName={repoGroup.repoFullName} section="issues" tabKey={tabKey()} />
}
>
<div class="bg-base-100" data-repo-group={repoGroup.repoFullName}>
Expand All @@ -375,7 +376,7 @@ export default function IssuesTab(props: IssuesTabProps) {
trailing={
<>
<RepoGitHubLink repoFullName={repoGroup.repoFullName} section="issues" />
<RepoLockControls repoFullName={repoGroup.repoFullName} />
<RepoLockControls repoFullName={repoGroup.repoFullName} tabKey={tabKey()} />
</>
}
collapsedSummary={
Expand Down
13 changes: 7 additions & 6 deletions src/app/components/dashboard/PullRequestsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,12 +251,13 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {

const repoGroups = createMemo(() => {
const groups = groupByRepo(filteredSorted());
const lockedForTab = viewState.lockedRepos[tabKey()] ?? [];
const withLocked = ensureLockedRepoGroups(
groups,
viewState.lockedRepos,
lockedForTab,
(name) => ({ repoFullName: name, items: [] as typeof groups[0]["items"] }),
);
return orderRepoGroups(withLocked, viewState.lockedRepos);
return orderRepoGroups(withLocked, lockedForTab);
});
const pageLayout = createMemo(() => computePageLayout(repoGroups(), config.itemsPerPage));
const pageCount = createMemo(() => pageLayout().pageCount);
Expand All @@ -282,7 +283,7 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
createEffect(() => {
const names = activeRepoNames();
if (names.length === 0) return;
pruneLockedRepos(names);
pruneLockedRepos(tabKey(), names);
});

const { flashingIds: flashingPRIds, peekUpdates } = createFlashDetection({
Expand All @@ -302,7 +303,7 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {

const highlightedReposPRs = createReorderHighlight(
() => repoGroups().map(g => g.repoFullName),
() => viewState.lockedRepos,
() => viewState.lockedRepos[tabKey()] ?? [],
() => ignoredPullRequests().length,
() => JSON.stringify(props.customTabId
? (viewState.customTabFilters[props.customTabId] ?? {})
Expand Down Expand Up @@ -429,7 +430,7 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
<Show
when={!isEmpty()}
fallback={
<EmptyLockedRepoRow repoFullName={repoGroup.repoFullName} section="pulls" />
<EmptyLockedRepoRow repoFullName={repoGroup.repoFullName} section="pulls" tabKey={tabKey()} />
}
>
<div class="bg-base-100" data-repo-group={repoGroup.repoFullName}>
Expand All @@ -449,7 +450,7 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
trailing={
<>
<RepoGitHubLink repoFullName={repoGroup.repoFullName} section="pulls" />
<RepoLockControls repoFullName={repoGroup.repoFullName} />
<RepoLockControls repoFullName={repoGroup.repoFullName} tabKey={tabKey()} />
</>
}
collapsedSummary={
Expand Down
3 changes: 2 additions & 1 deletion src/app/components/shared/EmptyLockedRepoRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import RepoLockControls from "./RepoLockControls";
export default function EmptyLockedRepoRow(props: {
repoFullName: string;
section: "issues" | "pulls" | "actions";
tabKey: string;
}) {
return (
<div
Expand All @@ -15,7 +16,7 @@ export default function EmptyLockedRepoRow(props: {
<span class="text-sm text-base-content/60">{props.repoFullName}</span>
</span>
<RepoGitHubLink repoFullName={props.repoFullName} section={props.section} />
<RepoLockControls repoFullName={props.repoFullName} />
<RepoLockControls repoFullName={props.repoFullName} tabKey={props.tabKey} />
</div>
);
}
11 changes: 6 additions & 5 deletions src/app/components/shared/RepoLockControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import { withFlipAnimation } from "../../lib/scroll";

interface RepoLockControlsProps {
repoFullName: string;
tabKey: string;
}

export default function RepoLockControls(props: RepoLockControlsProps) {
const lockInfo = createMemo(() => {
const list = viewState.lockedRepos;
const list = viewState.lockedRepos[props.tabKey] ?? [];
const idx = list.indexOf(props.repoFullName);
return {
isLocked: idx !== -1,
Expand All @@ -26,7 +27,7 @@ export default function RepoLockControls(props: RepoLockControlsProps) {
<Tooltip content="Pin to top">
<button
class="btn btn-ghost btn-xs opacity-0 group-hover/repo-header:opacity-100 focus:opacity-100 max-sm:opacity-60 sm:max-lg:opacity-60 transition-opacity"
onClick={() => withFlipAnimation(() => lockRepo(props.repoFullName))}
onClick={() => withFlipAnimation(() => lockRepo(props.tabKey, props.repoFullName))}
aria-label={`Pin ${props.repoFullName} to top of list`}
>
{/* Heroicons 20px solid: lock-open */}
Expand All @@ -40,7 +41,7 @@ export default function RepoLockControls(props: RepoLockControlsProps) {
<Tooltip content="Unpin">
<button
class="btn btn-ghost btn-xs"
onClick={() => withFlipAnimation(() => unlockRepo(props.repoFullName))}
onClick={() => withFlipAnimation(() => unlockRepo(props.tabKey, props.repoFullName))}
aria-label={`Unpin ${props.repoFullName}`}
>
{/* Heroicons 20px solid: lock-closed */}
Expand All @@ -52,7 +53,7 @@ export default function RepoLockControls(props: RepoLockControlsProps) {
<Tooltip content={lockInfo().isFirst ? "Already at top of pinned list" : "Move up"}>
<button
class="btn btn-ghost btn-xs"
onClick={() => withFlipAnimation(() => moveLockedRepo(props.repoFullName, "up"))}
onClick={() => withFlipAnimation(() => moveLockedRepo(props.tabKey, props.repoFullName, "up"))}
disabled={lockInfo().isFirst}
aria-label={`Move ${props.repoFullName} up`}
>
Expand All @@ -65,7 +66,7 @@ export default function RepoLockControls(props: RepoLockControlsProps) {
<Tooltip content={lockInfo().isLast ? "Already at bottom of pinned list" : "Move down"}>
<button
class="btn btn-ghost btn-xs"
onClick={() => withFlipAnimation(() => moveLockedRepo(props.repoFullName, "down"))}
onClick={() => withFlipAnimation(() => moveLockedRepo(props.tabKey, props.repoFullName, "down"))}
disabled={lockInfo().isLast}
aria-label={`Move ${props.repoFullName} down`}
>
Expand Down
Loading