Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c10f1d1

Browse files
authored
extend-repo-tabs - New feature (refined-github#9902)
1 parent 18b5b56 commit c10f1d1

12 files changed

Lines changed: 188 additions & 35 deletions

build/__snapshots__/features-meta.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,11 @@
281281
"css": true,
282282
"screenshot": "https://user-images.githubusercontent.com/1402241/152118201-f25034c7-6fae-4be0-bb3f-c217647e32b7.gif"
283283
},
284+
{
285+
"id": "extend-repo-tabs",
286+
"description": "Collapses repository tabs if rarely used (Insights, Security) or when empty (Projects, Wiki, Actions). It also adds counters to Projects and Wiki.",
287+
"screenshot": "https://github.com/user-attachments/assets/e7fa93ff-fc01-410d-b4da-f1fb96a85326"
288+
},
284289
{
285290
"id": "extensible-nav",
286291
"description": "Beta: This is a core feature that enables other Refined GitHub features to extend or modify the repository navigation bar. Currently opt-in for testing.",

build/__snapshots__/imported-features.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"expand-all-hidden-comments",
4949
"extend-conversation-status-filters",
5050
"extend-diff-expander",
51+
"extend-repo-tabs",
5152
"extensible-nav",
5253
"file-age-color",
5354
"fit-textareas",

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ https://github.com/refined-github/refined-github/wiki/Contributing#metadata-guid
107107
- [](# "visit-tag") [When navigating a repo's file on a specific tag, it adds a link to see the release/tag itself.](https://github-production-user-asset-6210df.s3.amazonaws.com/1402241/285123739-e5f4fa0a-3f48-49ef-9b87-2fd6f183c923.png)
108108
- [](# "actions-run-removal") [Lets you cancel or delete workflow runs faster from the workflow list.](https://github.com/user-attachments/assets/a054f9b4-9d56-40c0-9aac-09a8b07bbb3b)
109109
- [](# "rerun-workflow") [Unwraps the "Re-run jobs" dropdown into individual buttons and adds a keyboard shortcut to re-run failed jobs: <kbd>r</kbd> <kbd>f</kbd>](https://github.com/user-attachments/assets/67331112-f5b2-4a2b-af43-800d46bd6bf7).
110+
- [](# "extend-repo-tabs") [Collapses repository tabs if rarely used (Insights, Security) or when empty (Projects, Wiki, Actions). It also adds counters to Projects and Wiki.](https://github.com/user-attachments/assets/e7fa93ff-fc01-410d-b4da-f1fb96a85326)
110111

111112
<!--
112113
Refer to style guide in the wiki. Keep this message between sections.

source/components/extensible-nav-store.test.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,27 @@ it('inserts multiple extra tabs before the same native tab in call order', async
6262
expect(get(tabs).map(tab => tab.id)).toEqual(['code', 'bugs', 'triage', 'issues']);
6363
});
6464

65-
it('hides a native tab', async () => {
66-
const {tabs, setNativeTabs, hideTab} = await loadModule();
67-
setNativeTabs([makeTab('code'), makeTab('issues')]);
68-
hideTab('issues');
69-
expect(get(tabs).map(tab => tab.id)).toEqual(['code']);
65+
it('overrides a native tab label', async () => {
66+
const {tabs, setNativeTabs, overrideTab} = await loadModule();
67+
setNativeTabs([makeTab('security-and-quality', {label: 'Security and quality'})]);
68+
overrideTab('security-and-quality', {label: 'Security'});
69+
expect(get(tabs)[0].label).toBe('Security');
7070
});
7171

72-
it('does not hide extra tabs (only filters native tabs by id)', async () => {
73-
const {tabs, setNativeTabs, addTab, hideTab} = await loadModule();
74-
setNativeTabs([makeTab('code')]);
75-
addTab(makeTab('bugs'));
76-
hideTab('bugs');
77-
expect(get(tabs).map(tab => tab.id)).toEqual(['code', 'bugs']);
72+
it('merges multiple overrides for the same tab', async () => {
73+
const {tabs, setNativeTabs, overrideTab} = await loadModule();
74+
setNativeTabs([makeTab('agents')]);
75+
overrideTab('agents', {label: 'AI agents'});
76+
overrideTab('agents', {demoted: true});
77+
expect(get(tabs)[0]).toMatchObject({label: 'AI agents', demoted: true});
78+
});
79+
80+
it('moves demoted native tabs to the end, preserving relative order', async () => {
81+
const {tabs, setNativeTabs, overrideTab} = await loadModule();
82+
setNativeTabs([makeTab('code'), makeTab('projects'), makeTab('issues'), makeTab('insights')]);
83+
overrideTab('projects', {demoted: true});
84+
overrideTab('insights', {demoted: true});
85+
expect(get(tabs).map(tab => tab.id)).toEqual(['code', 'issues', 'projects', 'insights']);
7886
});
7987

8088
it('replacing native tabs does not drop extra tabs', async () => {

source/components/extensible-nav-store.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,33 @@ export type Tab = {
1010
reactNav?: string;
1111
counter?: Readable<number | string | undefined>;
1212
tooltip?: string;
13+
demoted?: true | string; // User-visible reason for demotion, e.g. "empty" or "disabled"
1314
selected?: () => boolean | Promise<boolean>;
1415
};
1516

1617
type ExtraTab = {tab: Tab; before?: string};
18+
type TabOverride = Partial<Pick<Tab, 'label' | 'counter' | 'tooltip' | 'demoted'>>;
1719

1820
const nativeTabs = writable<Tab[]>([]);
1921
const extraTabs = writable<ExtraTab[]>([]);
20-
const hiddenIds = writable(new Set<string>());
22+
const overrides = writable(new Map<string, TabOverride>());
2123

2224
export const selectedId = writable<string | undefined>();
2325

2426
export const tabs = derived(
25-
[nativeTabs, extraTabs, hiddenIds],
26-
([$nativeTabs, $extraTabs, $hiddenIds]) => {
27-
const tabs = $nativeTabs.filter(({id}) => !$hiddenIds.has(id));
27+
[nativeTabs, extraTabs, overrides],
28+
([$nativeTabs, $extraTabs, $overrides]) => {
29+
const tabs = $nativeTabs.map(tab => ({...tab, ...$overrides.get(tab.id)}));
2830

2931
for (const {tab, before} of $extraTabs) {
3032
const index = before ? tabs.findIndex(({id}) => id === before) : -1;
3133
tabs.splice(index === -1 ? tabs.length : index, 0, tab);
3234
}
3335

34-
return tabs;
36+
const demoted = tabs.filter(tab => tab.demoted);
37+
const rest = tabs.filter(tab => !tab.demoted);
38+
39+
return [...rest, ...demoted];
3540
},
3641
);
3742

@@ -56,8 +61,8 @@ export function addTab(tab: Tab, before?: string): void {
5661
}
5762
}
5863

59-
export function hideTab(id: string): void {
60-
hiddenIds.update(current => new Set(current).add(id));
64+
export function overrideTab(id: string, override: TabOverride): void {
65+
overrides.update(current => new Map(current).set(id, {...current.get(id), ...override}));
6166
}
6267

6368
export async function updateCurrentTab(): Promise<void> {

source/features/clean-conversation-filters.gql

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
1-
query HasAnyProjects($owner: String!, $name: String!) {
1+
query GetProjectCount($owner: String!, $name: String!) {
22
repository(owner: $owner, name: $name) {
3-
projects {
4-
totalCount
5-
}
63
projectsV2 {
74
totalCount
85
}
96
}
107
organization(login: $owner) {
11-
projects {
12-
totalCount
13-
}
148
projectsV2 {
159
totalCount
1610
}

source/features/clean-conversation-filters.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,7 @@ const hasAnyProjects = new CachedFunction('has-projects', {
3030
allowErrors: true,
3131
});
3232

33-
return Boolean(repository.projects.totalCount)
34-
|| Boolean(repository.projectsV2.totalCount)
35-
// Joint query, both org and projects are optional
36-
|| Boolean(organization?.projects?.totalCount)
33+
return Boolean(repository.projectsV2.totalCount)
3734
// Joint query, both org and projects are optional
3835
|| Boolean(organization?.projectsV2?.totalCount);
3936
},
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
query RepoCountInfo($owner: String!, $name: String!) {
2+
repository(owner: $owner, name: $name) {
3+
projectsV2 {
4+
totalCount
5+
}
6+
defaultBranchRef {
7+
target {
8+
... on Commit {
9+
checkSuites {
10+
totalCount
11+
}
12+
}
13+
}
14+
}
15+
}
16+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import * as pageDetect from 'github-url-detection';
2+
import {CachedFunction} from 'webext-storage-cache';
3+
import {writable} from 'svelte/store';
4+
5+
import features from '../feature-manager.js';
6+
import api from '../github-helpers/api.js';
7+
import {cacheByRepo, buildRepoUrl} from '../github-helpers/index.js';
8+
import fetchDom from '../helpers/fetch-dom.js';
9+
import looseParseInt from '../helpers/loose-parse-int.js';
10+
import {overrideTab} from '../components/extensible-nav-store.js';
11+
import {expectTokenScope} from '../github-helpers/github-token.js';
12+
import RepoCountInfo from './extend-repo-tabs.gql';
13+
14+
type RepoTabsCounts = {
15+
projects: number;
16+
actionRuns: number;
17+
};
18+
19+
const cacheOptions = {
20+
maxAge: {days: 1},
21+
staleWhileRevalidate: {days: 10},
22+
cacheKey: cacheByRepo,
23+
} as const;
24+
25+
const repoTabsCounts = new CachedFunction('repo-tabs-counts', {
26+
async updater(): Promise<RepoTabsCounts> {
27+
await expectTokenScope('read:project');
28+
const {repository} = await api.v4(RepoCountInfo);
29+
30+
return {
31+
// Projects undefined if not enabled in the repo
32+
projects: repository.projectsV2?.totalCount ?? 0,
33+
actionRuns: repository.defaultBranchRef.target.checkSuites.totalCount,
34+
};
35+
},
36+
...cacheOptions,
37+
});
38+
39+
const wikiPageCount = new CachedFunction('wiki-page-count', {
40+
async updater(): Promise<number> {
41+
// No v3/v4 API access at all
42+
const counter = await fetchDom(buildRepoUrl('wiki'), '#wiki-pages-box .Counter');
43+
return looseParseInt(counter);
44+
},
45+
...cacheOptions,
46+
});
47+
48+
async function updateWikiTab(): Promise<void> {
49+
const count = await wikiPageCount.get();
50+
if (count > 0) {
51+
overrideTab('wiki', {counter: writable(count)});
52+
} else {
53+
overrideTab('wiki', {demoted: 'empty'});
54+
}
55+
}
56+
57+
async function updateActionsAndProjectsTabs(): Promise<void> {
58+
const {projects, actionRuns} = await repoTabsCounts.get();
59+
60+
if (actionRuns === 0) {
61+
overrideTab('actions', {demoted: 'unused'});
62+
}
63+
64+
if (projects > 0) {
65+
overrideTab('projects', {counter: writable(projects)});
66+
} else {
67+
overrideTab('projects', {demoted: 'none'});
68+
}
69+
}
70+
71+
function init(): void {
72+
overrideTab('pull-requests', {label: 'Pulls'});
73+
overrideTab('agents', {demoted: true});
74+
overrideTab('security-and-quality', {demoted: true});
75+
overrideTab('insights', {demoted: true});
76+
77+
void updateWikiTab();
78+
void updateActionsAndProjectsTabs();
79+
}
80+
81+
void features.add(import.meta.url, {
82+
include: [
83+
pageDetect.hasRepoHeader,
84+
],
85+
// The feature partially works without a token
86+
// requiresToken: true,
87+
init,
88+
});
89+
90+
/*
91+
92+
Test URLs:
93+
94+
- Repo with 0 projects: https://github.com/babel/flavortown
95+
- Repo with some projects: https://github.com/github/docs/projects
96+
- Repo with 0 wiki: https://github.com/babel/babel-sublime-snippets
97+
- Repo with some wiki: https://github.com/refined-github/refined-github
98+
- Repo with 0 actions: https://github.com/babel/jade-babel
99+
- Repo with security alerts: https://github.com/babel/babel
100+
101+
*/

source/features/extensible-nav.svelte

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,49 @@
99
<ul class="UnderlineNav-body">
1010
{#each $tabs as tab (tab.id)}
1111
{@const id = `rgh-extensible-nav-${tab.id}`}
12-
<li>
12+
{@const isSelected = tab.id === $selectedId}
13+
<!-- Always show label if selected -->
14+
{@const hasLabel = !tab.demoted || isSelected}
15+
{@const tooltip = tab.tooltip ?? (hasLabel ? undefined : tab.label)}
16+
<!-- Keep it demoted even if it has a forced label due to `isSelected` -->
17+
<li class:demoted={tab.demoted}>
1318
<a
1419
{id}
1520
href={tab.href}
1621
class="UnderlineNav-item"
1722
data-turbo-frame="repo-content-turbo-frame"
1823
data-react-nav={tab.reactNav}
19-
class:selected={tab.id === $selectedId}
20-
aria-labelledby={tab.tooltip ? `${id}-tooltip` : undefined}
24+
aria-labelledby={tooltip ? `${id}-tooltip` : undefined}
25+
class:selected={isSelected}
2126
>
2227
<DomChef as={tab.icon} class="UnderlineNav-octicon" />
23-
<span data-content={tab.label}>{tab.label}</span>
28+
<!-- Don't use [hidden] because Svelte won't render it at all -->
29+
<span class:d-none={!hasLabel} data-content={tab.label}>
30+
{tab.label}
31+
</span>
2432
<TabCounter counter={tab.counter} />
2533
</a>
26-
{#if tab.tooltip}
27-
<Tooltip id="{id}-tooltip" htmlFor={id} label={tab.tooltip} />
34+
{#if tooltip}
35+
<Tooltip
36+
id="{id}-tooltip"
37+
htmlFor={id}
38+
label={typeof tab.demoted === 'string' ? `${tab.label}: ${tab.demoted}` : tooltip}
39+
/>
2840
{/if}
2941
</li>
3042
{/each}
3143
</ul>
3244
</nav>
3345
<style>
46+
a {
47+
min-height: 1lh;
48+
gap: 8px;
49+
50+
:global(svg, .Counter) {
51+
margin: 0;
52+
}
53+
}
54+
.demoted:nth-child(1 of .demoted) {
55+
margin-left: 16px;
56+
}
3457
</style>

0 commit comments

Comments
 (0)