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
131
132
133
134
135
136
137
138
139
|
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*
* This Source Code Form is "Incompatible With Secondary Licenses", as
* defined by the Mozilla Public License, v. 2.0. */
/**
* Reference or define the Bugzilla app namespace.
* @namespace
*/
var Bugzilla = Bugzilla || {};
/**
* Reference or define the Review namespace.
* @namespace
*/
Bugzilla.Review = Bugzilla.Review || {};
/**
* Provide the Badge functionality that shows the current review summary in the dropdown.
*/
Bugzilla.Review.Badge = class Badge {
/**
* Get a new Badge instance.
* @returns {Badge} New Badge instance.
*/
constructor() {
this.initialized = false;
this.$button = document.querySelector('#header-requests-menu-button');
this.$panel = document.querySelector('#header-requests .dropdown-panel');
this.$loading = document.querySelector('#header-requests .dropdown-panel .loading');
if (this.$loading) {
this.$button.addEventListener('mouseover', () => this.init(), { once: true });
this.$button.addEventListener('focus', () => this.init(), { once: true });
}
}
/**
* Initialize the Reviews dropdown menu.
*/
async init() {
if (this.initialized) {
return;
}
this.initialized = true;
const url = this.$panel.querySelector('footer a').href.replace(/type$/, 'requestee') + '&ctype=json';
const response = await fetch(url, { credentials: 'same-origin' });
const _requests = response.ok ? await response.json() : [];
if (!response.ok) {
this.$loading.innerHTML = 'Couldn’t load requests for you.<br>Please try again later.';
return;
}
if (!_requests.length) {
this.$loading.className = 'empty';
this.$loading.innerHTML = 'You’re all caught up!';
return;
}
const requests = [];
const $ul = this.$panel.querySelector('ul');
const $fragment = document.createDocumentFragment();
// Sort requests from new to old, then group reviews/feedbacks asked by the same person in the same bug
_requests.reverse().forEach(_req => {
const dup_index = requests.findIndex(req => req.requester === _req.requester && req.type === _req.type
&& req.bug_id === _req.bug_id && req.attach_mimetype === _req.attach_mimetype);
if (dup_index > -1) {
requests[dup_index].dup_count++;
} else {
_req.dup_count = 1;
requests.push(_req);
}
});
// Show up to 20 newest requests
requests.slice(0, 20).forEach(req => {
const $li = document.createElement('li');
const [, name, email] = req.requester.match(/^(?:(.*)\s<)?(.+?)>?$/);
const pretty_name = name ? name.replace(/([\[\(<‹].*?[›>\)\]]|\:[\w\-]+|\s+\-\s+.*)/g, '').trim() : email;
const [link, attach_label] = this.get_link(req);
$li.setAttribute('role', 'none');
$li.innerHTML = `<a href="${link.htmlEncode()}" role="menuitem" tabindex="-1" `
+ `class="${(req.restricted ? 'secure' : '')}" data-type="${req.type}">`
+ `<img src="https://secure.gravatar.com/avatar/${md5(email.toLowerCase())}?d=mm&size=64" alt="">`
+ `<label><strong>${pretty_name.htmlEncode()}</strong> asked for your `
+ (req.type === 'needinfo' ? 'info' : req.type) + (attach_label ? ` on ${attach_label}` : '')
+ ' in ' + (req.restricted ? '<span class="icon" aria-label="secure"></span> ' : '')
+ `<strong>Bug ${req.bug_id} – ${req.bug_summary.htmlEncode()}</strong>.</label>`
+ `<time datetime="${req.created}">${timeAgo(new Date(req.created))}</time></a>`;
$fragment.appendChild($li);
});
this.$loading.remove();
$ul.appendChild($fragment);
$ul.hidden = false;
}
/**
* Get the link to a request as well as the label of any attachment. It could be the direct link to the attachment
* unless multiple requests are grouped.
* @param {Object} req - A request object.
* @returns {Array<String>} The result including the link and attachment label.
*/
get_link(req) {
const dup = req.dup_count > 1;
const splinter_base = BUGZILLA.param.splinter_base;
const x_types = ['github-pull-request', 'review-board-request', 'phabricator-request', 'google-doc'];
const is_patch = req.attach_ispatch;
const [is_ghpr, is_rbr, is_phr, is_gdoc] = x_types.map(type => req.attach_mimetype === `text/x-${type}`);
const is_redirect = is_ghpr || is_rbr || is_phr || is_gdoc;
const is_file = req.attach_id && !is_patch && !is_redirect;
const link = (is_patch && !dup && splinter_base)
? `${splinter_base}&bug=${req.bug_id}&attachment=${req.attach_id}`
: (is_redirect && !dup) ? `attachment.cgi?id=${req.attach_id}` // external redirect
: ((is_patch || is_file) && !dup) ? `attachment.cgi?id=${req.attach_id}&action=edit`
: `show_bug.cgi?id=${req.bug_id}`;
const attach_label = (is_patch || is_rbr || is_phr) ? (dup ? `${req.dup_count} patches` : 'a patch')
: is_ghpr ? (dup ? `${req.dup_count} pull requests` : 'a pull request')
: is_gdoc ? (dup ? `${req.dup_count} Google Docs` : 'a Google Doc')
: is_file ? (dup ? `${req.dup_count} files` : 'a file')
: undefined;
return [link, attach_label];
}
}
window.addEventListener('DOMContentLoaded', () => new Bugzilla.Review.Badge(), { once: true });
|