summaryrefslogtreecommitdiff
path: root/source/features/show-open-prs-of-forks.tsx
blob: 82fd909d64e7ccb6f7c210844b8d5886c0c4d874 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import React from 'dom-chef';
import {CachedFunction} from 'webext-storage-cache';
import {$} from 'select-dom';
import elementReady from 'element-ready';
import * as pageDetect from 'github-url-detection';

import features from '../feature-manager.js';
import api from '../github-helpers/api.js';
import pluralize from '../helpers/pluralize.js';
import {getForkedRepo, getUsername, getRepo} from '../github-helpers/index.js';
import GetPRs from './show-open-prs-of-forks.gql';

function getLinkCopy(count: number): string {
	return pluralize(count, 'one open pull request', 'at least $$ open pull requests');
}

const countPRs = new CachedFunction('prs-on-forked-repo', {
	async updater(forkedRepo: string): Promise<{count: number; firstPr?: number}> {
		const {search} = await api.v4(GetPRs, {
			variables: {
				query: `is:pr is:open archived:false repo:${forkedRepo} author:${getUsername()!}`,
			},
		});

		// Only show PRs originated from the current repo
		const prs = search.nodes.filter((pr: AnyObject) => pr.headRepository.nameWithOwner === getRepo()!.nameWithOwner);

		// If only one is found, pass the PR number so we can link to the PR directly
		if (prs.length === 1) {
			return {count: 1, firstPr: prs[0].number};
		}

		return {count: prs.length};
	},
	maxAge: {hours: 1},
	staleWhileRevalidate: {days: 2},
	cacheKey: ([forkedRepo]): string => `${forkedRepo}:${getRepo()!.nameWithOwner}`,
});

// eslint-disable-next-line @typescript-eslint/ban-types
async function getPRs(): Promise<[prCount: number, url: string] | []> {
	// Wait for the tab bar to be loaded
	await elementReady('.UnderlineNav-body');
	if (!pageDetect.canUserEditRepo()) {
		return [];
	}

	const forkedRepo = getForkedRepo()!;
	const {count, firstPr} = await countPRs.get(forkedRepo);
	if (count === 1) {
		return [count, `/${forkedRepo}/pull/${firstPr!}`];
	}

	const url = new URL(`/${forkedRepo}/pulls`, location.origin);
	url.searchParams.set('q', 'is:pr is:open sort:updated-desc author:@me');
	return [count, url.href];
}

async function initHeadHint(): Promise<void | false> {
	const [count, url] = await getPRs();
	if (!count) {
		return false;
	}

	$(`[data-hovercard-type="repository"][href="/${getForkedRepo()!}"]`)!.after(
		// The class is used by `quick-fork-deletion`
		<> with <a href={url} className="rgh-open-prs-of-forks">{getLinkCopy(count)}</a></>,
	);
}

async function initDeleteHint(): Promise<void | false> {
	const [count, url] = await getPRs();
	if (!count) {
		return false;
	}

	$('details-dialog[aria-label*="Delete"] .Box-body p:first-child')!.after(
		<p className="flash flash-warn">
			It will also abandon <a href={url}>your {getLinkCopy(count)}</a> in <strong>{getForkedRepo()!}</strong> and you’ll no longer be able to edit {count === 1 ? 'it' : 'them'}.
		</p>,
	);
}

void features.add(import.meta.url, {
	asLongAs: [
		pageDetect.isForkedRepo,
	],
	deduplicate: 'has-rgh',
	init: initHeadHint,
}, {
	asLongAs: [
		pageDetect.isForkedRepo,
	],
	include: [
		pageDetect.isRepoMainSettings,
	],
	deduplicate: 'has-rgh',
	init: initDeleteHint,
});

/*

Test URLs:

1. Visit https://github.com/pulls?q=is%3Apr+is%3Aopen+author%3A%40me+archived%3Afalse+-user%3A%40me
2. Find a PR made from a fork
3. In it, open your own fork

*/