Skip to content

Commit 92db121

Browse files
authored
Meta: Use :has() selector or remove related comments (refined-github#7554)
1 parent fde705f commit 92db121

10 files changed

Lines changed: 88 additions & 86 deletions

.github/workflows/release.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ jobs:
2323
- run: npm ci
2424
- name: Test and build
2525
run: npm run build
26-
# TODO: Re-enable tests
2726
- name: Create tag if necessary
2827
uses: fregante/daily-version-action@v2
2928
- name: Update manifest.json with version ${{ env.DAILY_VERSION}}

source/features/conversation-activity-filter.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,6 @@ function createRadios(current: State): JSX.Element[] {
132132
}
133133

134134
async function addWidget(state: State, anchor: HTMLElement): Promise<void> {
135-
// TODO: use :has instead
136135
const position = anchor.closest('div')!;
137136
if (position.classList.contains('rgh-conversation-activity-filter')) {
138137
return;

source/features/dim-bots.tsx

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,37 +6,7 @@ import delegate, {DelegateEvent} from 'delegate-it';
66
import features from '../feature-manager.js';
77
import preserveScroll from '../helpers/preserve-scroll.js';
88
import observe from '../helpers/selector-observer.js';
9-
10-
const botNames = [
11-
'actions-user',
12-
'bors',
13-
'ImgBotApp',
14-
'renovate-bot',
15-
'rust-highfive',
16-
'scala-steward',
17-
'weblate',
18-
'apps', // Matches any `/apps/*` URLs
19-
] as const;
20-
21-
// All co-authored commits are excluded because it's unlikely that any bot co-authors with another bot, but instead they're co-authored with a human. In that case we don't want to dim the commit.
22-
// ^= is needed to match /apps/* URLs
23-
const commitSelectors = [
24-
// Co-authored commits are excluded because their avatars are not linked
25-
...botNames.map(bot => `a[data-testid="avatar-icon-link"][href^="/${bot}"]`),
26-
27-
// Legacy view, still used by PR commits
28-
// :only-child excludes co-authored commits
29-
...botNames.map(bot => `a[data-test-selector="commits-avatar-stack-avatar-link"][href^="/${bot}"]:only-child`),
30-
];
31-
32-
const prSelectors = [
33-
...botNames.flatMap(bot => [
34-
`.opened-by [title$="pull requests created by ${bot}"]`,
35-
`.opened-by [title$="pull requests opened by ${bot}"]`,
36-
]),
37-
'.opened-by [href*="author%3Aapp%2F"]', // Search query `is:pr+author:app/*`
38-
'.labels [href$="label%3Abot"]', // PR tagged with `bot` label
39-
];
9+
import {botLinksCommitSelectors, botLinksPrSelectors} from '../github-helpers/selectors.js';
4010

4111
const dimBots = features.getIdentifiers(import.meta.url);
4212

@@ -55,21 +25,15 @@ function undimBots(event: DelegateEvent): void {
5525
resetScroll();
5626
}
5727

58-
function dimCommit(commit: HTMLElement): void {
28+
function dim(commit: HTMLElement): void {
5929
commit.closest([
60-
'.listviewitem',
61-
'.Box-row', // Old view style before Nov 2023
30+
'.listviewitem', // Commits
31+
'.Box-row', // PRs
6232
])!.classList.add(dimBots.class);
6333
}
6434

65-
function dimPr(pr: HTMLElement): void {
66-
// TODO: Use :has selector and merge into a single `selectors` array
67-
pr.closest('.Box-row')!.classList.add(dimBots.class);
68-
}
69-
7035
async function init(signal: AbortSignal): Promise<void> {
71-
observe(commitSelectors, dimCommit, {signal});
72-
observe(prSelectors, dimPr, {signal});
36+
observe([...botLinksCommitSelectors, ...botLinksPrSelectors], dim, {signal});
7337

7438
// Undim on mouse focus
7539
delegate(dimBots.selector, 'click', undimBots, {signal});

source/features/hide-inactive-deployments.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ function init(): void {
1010
deployments.pop(); // Don't hide the last deployment, even if it is inactive
1111

1212
for (const deployment of deployments) {
13-
// TODO: Rewrite with :has selector, CSS-only feature
1413
if (elementExists('[title="Deployment Status Label: Inactive"]', deployment)) {
1514
deployment.remove();
1615
}
@@ -21,6 +20,14 @@ void features.add(import.meta.url, {
2120
include: [
2221
pageDetect.isPRConversation,
2322
],
24-
awaitDomReady: true, // TODO: Rewrite with :has selector, CSS-only feature
23+
awaitDomReady: true,
2524
init,
2625
});
26+
27+
/*
28+
29+
Test URLs:
30+
31+
https://github.com/btkostner/btkostner.io/pull/10
32+
33+
*/

source/features/hide-low-quality-comments.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ function init(): void {
3636
hideComment(similarCommentsBox);
3737
}
3838

39-
const linkedComment = location.hash.startsWith('#issuecomment-') ? $(`${location.hash} ${singleParagraphCommentSelector}`) : undefined;
39+
const linkedComment = location.hash.startsWith('#issuecomment-')
40+
? $(`${location.hash} ${singleParagraphCommentSelector}`)
41+
: undefined;
4042

4143
for (const commentText of $$(singleParagraphCommentSelector)) {
4244
// Exclude explicitly linked comments #5363
@@ -50,13 +52,11 @@ function init(): void {
5052

5153
// Comments that contain useful images or links shouldn't be removed
5254
// Images are wrapped in <a> tags on GitHub hence included in the selector
53-
// TODO: use :has()
5455
if (elementExists('a', commentText)) {
5556
continue;
5657
}
5758

5859
// Ensure that they're not by VIPs (owner, collaborators, etc)
59-
// TODO: use :has()
6060
const comment = commentText.closest('.js-timeline-item')!;
6161
if (elementExists('.Label', comment)) {
6262
continue;
@@ -88,7 +88,7 @@ function init(): void {
8888
}
8989
}
9090

91-
// This should not be made dynamic via observer, it's not worth updating the lowQuality count for fresh comments
91+
// This should NOT be made dynamic via observer, it's not worth updating the lowQuality count for fresh comments
9292
void features.add(import.meta.url, {
9393
include: [
9494
pageDetect.isIssue,

source/features/one-click-diff-options.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,7 @@ function createWhitespaceButton(): HTMLElement {
3636
);
3737
}
3838

39-
function attachPRButtons(dropdownIcon: SVGElement): void {
40-
// TODO: Replace with :has selector
41-
const dropdown = dropdownIcon.closest('details.diffbar-item')!;
39+
function attachPRButtons(dropdown: HTMLDetailsElement): void {
4240
const diffSettingsForm = $('form[action$="/diffview"]', dropdown)!;
4341

4442
// Preserve data before emption the form
@@ -98,11 +96,10 @@ function attachPRButtons(dropdownIcon: SVGElement): void {
9896

9997
function initPR(signal: AbortSignal): void {
10098
// There are two "diff settings" element, one for mobile and one for the desktop. We only replace the one for the desktop
101-
observe('.hide-sm.hide-md details.diffbar-item svg.octicon-gear', attachPRButtons, {signal});
99+
observe('.hide-sm.hide-md details.diffbar-item:has(svg.octicon-gear)', attachPRButtons, {signal});
102100
}
103101

104102
function attachButtons(nativeDiffButtons: HTMLElement): void {
105-
// TODO: Replace with :has()
106103
const anchor = nativeDiffButtons.parentElement!;
107104

108105
// `usesFloats` is necessary to ensure the order and spacing as seen in #5958

source/features/open-all-conversations.tsx

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,45 @@
11
import React from 'dom-chef';
2-
import {$$, elementExists} from 'select-dom';
2+
import {$$} from 'select-dom';
33
import delegate, {DelegateEvent} from 'delegate-it';
44
import elementReady from 'element-ready';
55
import * as pageDetect from 'github-url-detection';
66

77
import features from '../feature-manager.js';
88
import openTabs from '../helpers/open-tabs.js';
9-
import {attachElements} from '../helpers/attach-element.js';
10-
11-
function getUrlFromItem(issue: Element): string {
12-
return issue
13-
.closest('.js-issue-row')!
14-
.querySelector('a.js-navigation-open')!
15-
.href;
16-
}
9+
import observe from '../helpers/selector-observer.js';
1710

1811
const issueListSelector = pageDetect.isGlobalIssueOrPRList()
1912
? '#js-issues-toolbar div'
2013
: 'div[aria-label="Issues"][role="group"]';
2114

2215
function onButtonClick(event: DelegateEvent<MouseEvent, HTMLButtonElement>): void {
23-
const onlySelected = event.delegateTarget.closest('.table-list-triage');
24-
const issues = $$(`${issueListSelector} .js-issue-row`)
25-
// TODO: Use conditional :has(:checked) instead
26-
.filter(issue => onlySelected ? elementExists(':checked', issue) : true);
27-
void openTabs(issues.map(issue => getUrlFromItem(issue)));
16+
const onlySelected = event.delegateTarget.closest('.table-list-triage')
17+
? ':has(:checked)'
18+
: '';
19+
20+
const issueSelector = `${issueListSelector} .js-issue-row${onlySelected} a.js-navigation-open`;
21+
22+
const urls = $$(issueSelector as 'a').map(issue => issue.href);
23+
void openTabs(urls);
2824
}
2925

3026
async function hasMoreThanOneConversation(): Promise<boolean> {
3127
return Boolean(await elementReady('.js-issue-row + .js-issue-row', {waitForChildren: false}));
3228
}
3329

34-
async function init(signal: AbortSignal): Promise<void | false> {
35-
attachElements('.table-list-header-toggle:not(.states)', {
36-
prepend: anchor => (
37-
<button
38-
type="button"
39-
className="btn-link rgh-open-all-conversations px-2"
40-
>
41-
{anchor.closest('.table-list-triage') ? 'Open selected' : 'Open all'}
42-
</button>
43-
),
44-
});
30+
function add(anchor: HTMLElement): void {
31+
anchor.prepend(
32+
<button
33+
type="button"
34+
className="btn-link rgh-open-all-conversations px-2"
35+
>
36+
{anchor.closest('.table-list-triage') ? 'Open selected' : 'Open all'}
37+
</button>,
38+
);
39+
}
4540

41+
async function init(signal: AbortSignal): Promise<void | false> {
42+
observe('.table-list-header-toggle:not(.states)', add, {signal});
4643
delegate('button.rgh-open-all-conversations', 'click', onButtonClick, {signal});
4744
}
4845

source/features/release-download-count.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import './release-download-count.css';
22
import React from 'dom-chef';
3-
import {$$, elementExists} from 'select-dom';
3+
import {$$} from 'select-dom';
44
import DownloadIcon from 'octicons-plain-react/Download';
55
import * as pageDetect from 'github-url-detection';
66
import {abbreviateNumber} from 'js-abbreviation-number';
@@ -23,11 +23,6 @@ async function getAssetsForTag(tag: string): Promise<Record<string, number>> {
2323
}
2424

2525
async function addCounts(assetsList: HTMLElement): Promise<void> {
26-
// TODO: Use :has selector instead
27-
if (!elementExists('.octicon-package', assetsList)) {
28-
return;
29-
}
30-
3126
// Both pages have .Box but in the list .Box doesn't include the tag
3227
const container = assetsList.closest('section') // Single-release page
3328
?? assetsList.closest('.Box:not(.Box--condensed)')!; // Releases list, excludes the assets list’s own .Box
@@ -74,7 +69,7 @@ async function addCounts(assetsList: HTMLElement): Promise<void> {
7469
}
7570

7671
function init(signal: AbortSignal): void {
77-
observe('.Box-footer .Box--condensed', addCounts, {signal});
72+
observe('.Box-footer .Box--condensed:has(.octicon-package)', addCounts, {signal});
7873
}
7974

8075
void features.add(import.meta.url, {

source/features/warning-for-disallow-edits.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ function init(signal: AbortSignal): void | false {
3838
return false;
3939
}
4040

41+
// TODO: Rewrite with :has selector, maybe in https://github.com/refined-github/refined-github/issues/7574
4142
update(checkbox); // The sidebar checkbox may already be un-checked
4243
delegate('input[name="collab_privs"]', 'change', toggleHandler, {signal});
4344
}
@@ -51,3 +52,13 @@ void features.add(import.meta.url, {
5152
awaitDomReady: true,
5253
init,
5354
});
55+
56+
/*
57+
58+
Test URLs:
59+
60+
1. Open https://github.com/pulls?q=+is%3Apr+is%3Aopen+author%3A%40me+archived%3Afalse+-user%3A%40me+
61+
2. Open any PR opened from a fork
62+
3. Toggle the checkbox in the sidebar
63+
64+
*/

source/github-helpers/selectors.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,36 @@ export const linksToConversationLists_ = [
127127
[6, 'https://github.com/fregante/iphone-inline-video/issues?q=cool+is%3Aissue+is%3Aopen+'],
128128
[26, 'https://github.com/fregante/iphone-inline-video/issues?q=cool+is%3Aissue+is%3Aclosed'],
129129
] satisfies UrlMatch[];
130+
131+
const botNames = [
132+
'actions-user',
133+
'bors',
134+
'ImgBotApp',
135+
'renovate-bot',
136+
'rust-highfive',
137+
'scala-steward',
138+
'weblate',
139+
'apps', // Matches any `/apps/*` URLs
140+
] as const;
141+
142+
const botAttributes = botNames.map(bot => `[href^="/${bot}"]`).join(', ');
143+
144+
// All co-authored commits are excluded because it's unlikely that any bot co-authors with another bot, but instead they're co-authored with a human. In that case we don't want to dim the commit.
145+
// ^= is needed to match /apps/* URLs
146+
export const botLinksCommitSelectors = [
147+
// Co-authored commits are excluded because their avatars are not linked
148+
`a[data-testid="avatar-icon-link"]:is(${botAttributes})`,
149+
150+
// Legacy view, still used by PR commits
151+
// :only-child excludes co-authored commits
152+
`a[data-test-selector="commits-avatar-stack-avatar-link"]:is(${botAttributes}):only-child`,
153+
];
154+
155+
export const botLinksPrSelectors = [
156+
...botNames.flatMap(bot => [
157+
`.opened-by [title$="pull requests created by ${bot}"]`,
158+
`.opened-by [title$="pull requests opened by ${bot}"]`,
159+
]),
160+
'.opened-by [href*="author%3Aapp%2F"]', // Search query `is:pr+author:app/*`
161+
'.labels [href$="label%3Abot"]', // PR tagged with `bot` label
162+
];

0 commit comments

Comments
 (0)