summaryrefslogtreecommitdiff
path: root/source/features/pr-filters.tsx
blob: c781740e3544c5b7094f327aa7ed771afb26c2de (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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import React from 'dom-chef';
import cache from 'webext-storage-cache';
import select from 'select-dom';
import delegate from 'delegate-it';
import CheckIcon from 'octicon/check.svg';
import elementReady from 'element-ready';
import * as pageDetect from 'github-url-detection';

import features from '.';
import * as api from '../github-helpers/api';
import {getRepoGQL, getRepoURL} from '../github-helpers';

const reviewsFilterSelector = '#reviews-select-menu';

function addDropdownItem(dropdown: HTMLElement, title: string, filterCategory: string, filterValue: string): void {
	const filterQuery = `${filterCategory}:${filterValue}`;

	const searchParameter = new URLSearchParams(location.search);
	const currentQuerySegments = searchParameter.get('q')?.split(/\s+/) ?? [];
	const isSelected = currentQuerySegments.some(
		segment => segment.toLowerCase() === filterQuery
	);

	const query = currentQuerySegments.filter(
		segment => !segment.startsWith(`${filterCategory}:`)
	).join(' ');

	const search = new URLSearchParams({
		q: query + (isSelected ? '' : ` ${filterQuery}`)
	});

	dropdown.append(
		<a
			href={`?${String(search)}`}
			className="SelectMenu-item"
			aria-checked={isSelected ? 'true' : 'false'}
			role="menuitemradio"
		>
			<CheckIcon className="SelectMenu-icon SelectMenu-icon--check"/>
			<span>{title}</span>
		</a>
	);
}

const hasDraftFilter = new WeakSet();
function addDraftFilter({delegateTarget: reviewsFilter}: delegate.Event): void {
	if (hasDraftFilter.has(reviewsFilter)) {
		return;
	}

	hasDraftFilter.add(reviewsFilter);

	const dropdown = select('.SelectMenu-list', reviewsFilter)!;

	dropdown.append(
		<div className="SelectMenu-divider">
			Filter by draft pull requests
		</div>
	);

	addDropdownItem(dropdown, 'Ready for review', 'draft', 'false');
	addDropdownItem(dropdown, 'Not ready for review (Draft PR)', 'draft', 'true');
}

const hasChecks = cache.function(async (): Promise<boolean> => {
	const {repository} = await api.v4(`
		repository(${getRepoGQL()}) {
			head: object(expression: "HEAD") {
				... on Commit {
					history(first: 10) {
						nodes {
							statusCheckRollup {
								state
							}
						}
					}
				}
			}
		}
	`);

	return repository.head.history.nodes.some((commit: AnyObject) => commit.statusCheckRollup);
}, {
	maxAge: 3,
	cacheKey: () => __filebasename + ':' + getRepoURL()
});

async function addChecksFilter(): Promise<void> {
	const reviewsFilter = await elementReady(reviewsFilterSelector);
	if (!reviewsFilter) {
		return;
	}

	if (!await hasChecks()) {
		return;
	}

	// Copy existing element and adapt its content
	const checksFilter = reviewsFilter.cloneNode(true);
	checksFilter.id = '';

	select('summary', checksFilter)!.firstChild!.textContent = 'Checks\u00A0'; // Only replace text node, keep caret
	select('.SelectMenu-title', checksFilter)!.textContent = 'Filter by checks status';

	const dropdown = select('.SelectMenu-list', checksFilter)!;
	dropdown.textContent = ''; // Drop previous filters

	for (const status of ['Success', 'Failure', 'Pending']) {
		addDropdownItem(dropdown, status, 'status', status.toLowerCase());
	}

	reviewsFilter.after(checksFilter);
}

async function init(): Promise<void> {
	delegate(document, reviewsFilterSelector, 'toggle', addDraftFilter, true);
	await addChecksFilter();
}

void features.add({
	id: __filebasename,
	description: 'Adds Checks and Draft PR dropdown filters in PR lists.',
	screenshot: 'https://user-images.githubusercontent.com/202916/74453250-6d9de200-4e82-11ea-8fd4-7c0de57e001a.png'
}, {
	include: [
		pageDetect.isPRList
	],
	waitForDomReady: false,
	init
});