'use strict';
var popup, urls = [];
- function execute() {
+ async function execute() {
var type = $('#bitly-type').val();
if (urls[type]) {
}
$('#bitly-url').val('');
- var request = `${BUGZILLA.config.basepath}rest/bitly/${type}?` +
- `url=${encodeURIComponent($('#bitly-shorten').data('url'))}&` +
- `Bugzilla_api_token=${encodeURIComponent(BUGZILLA.api_token)}`;
- $.ajax(request)
- .done(function(data) {
- urls[type] = data.url;
- $('#bitly-url').val(urls[type]).select().focus();
- })
- .fail(function(data) {
- $('#bitly-url').val(data.responseJSON.message);
- });
+
+ try {
+ const { url } = await Bugzilla.API.get(`bitly/${type}`, { url: $('#bitly-shorten').data('url') });
+
+ urls[type] = url;
+ $('#bitly-url').val(urls[type]).select().focus();
+ } catch ({ message }) {
+ $('#bitly-url').val(message);
+ }
}
$('#bitly-shorten')
[%# === header === %]
-<div role="status" id="xhr-error" style="display:none"></div>
+<div role="status" id="io-error" style="display:none"></div>
<div role="status" id="floating-message" style="display:none">
<div id="floating-message-text"></div>
</div>
vertical-align: top;
}
-#xhr-error {
+#io-error {
margin: 5px 0;
border-radius: 2px;
border: 1px solid var(--error-message-foreground-color);
);
}
- function ccListUpdate() {
- bugzilla_ajax(
- {
- url: `${BUGZILLA.config.basepath}rest/bug_modal/cc/${BUGZILLA.bug_id}`
- },
- function(data) {
- $('#cc-list').html(data.html);
- $('#cc-latch').data('fetched', true);
- $('#cc-list .cc-user').hover(
- function() {
- $('#ccr-' + $(this).data('n')).css('visibility', 'visible');
- },
- function() {
- $('#ccr-' + $(this).data('n')).css('visibility', 'hidden');
+ async function ccListUpdate() {
+ try {
+ const { html } = await Bugzilla.API.get(`bug_modal/cc/${BUGZILLA.bug_id}`);
+
+ $('#io-error').empty().hide();
+ $('#cc-list').html(html);
+ $('#cc-latch').data('fetched', true);
+ $('#cc-list .cc-user').hover(
+ function() {
+ $('#ccr-' + $(this).data('n')).css('visibility', 'visible');
+ },
+ function() {
+ $('#ccr-' + $(this).data('n')).css('visibility', 'hidden');
+ }
+ );
+ $('#cc-list .show_usermenu').click(function() {
+ const $this = $(this);
+ return show_usermenu($this.data('user-id'), $this.data('user-email'), $this.data('show-edit'),
+ $this.data('hide-profile'));
+ });
+ $('#cc-list .cc-remove')
+ .click(function(event) {
+ event.preventDefault();
+ $('#top-save-btn').show();
+ var n = $(this).data('n');
+ var ccu = $('#ccu-' + n);
+ if (ccu.hasClass('cc-removed')) {
+ ccu.removeClass('cc-removed');
+ $('#cc-' + n).remove();
+ }
+ else {
+ $('#removecc').val('on');
+ ccu.addClass('cc-removed');
+ $('<input>').attr({
+ type: 'hidden',
+ id: 'cc-' + n,
+ value: $('#ccr-' + n).data('login'),
+ name: 'cc'
+ }).appendTo('#changeform');
}
- );
- $('#cc-list .show_usermenu').click(function() {
- const $this = $(this);
- return show_usermenu($this.data('user-id'), $this.data('user-email'), $this.data('show-edit'),
- $this.data('hide-profile'));
});
- $('#cc-list .cc-remove')
- .click(function(event) {
- event.preventDefault();
- $('#top-save-btn').show();
- var n = $(this).data('n');
- var ccu = $('#ccu-' + n);
- if (ccu.hasClass('cc-removed')) {
- ccu.removeClass('cc-removed');
- $('#cc-' + n).remove();
- }
- else {
- $('#removecc').val('on');
- ccu.addClass('cc-removed');
- $('<input>').attr({
- type: 'hidden',
- id: 'cc-' + n,
- value: $('#ccr-' + n).data('login'),
- name: 'cc'
- }).appendTo('#changeform');
- }
- });
- }
- );
+ } catch ({ message }) {
+ $('#io-error').html(message).show('fast');
+ }
}
if (BUGZILLA.user.id) {
// edit/save mode button
$('#mode-btn')
- .click(function(event) {
+ .click(async event => {
event.preventDefault();
// hide buttons, old error messages
$('#mode-btn').prop('disabled', true);
// load the missing select data
- bugzilla_ajax(
- {
- url: `${BUGZILLA.config.basepath}rest/bug_modal/edit/${BUGZILLA.bug_id}`
- },
- function(data) {
- $('#mode-btn').hide();
-
- // populate select menus
- Object.entries(data.options).forEach(([key, value]) => {
- const $select = document.querySelector(`#${key}`);
- if (!$select) return;
- // It can be radio-button-like UI
- const use_buttons = $select.matches('.buttons.toggle');
- const is_required = $select.matches('[aria-required="true"]');
- const selected = use_buttons ? $select.querySelector('input').value : $select.value;
- $select.innerHTML = '';
- value.forEach(({ name }) => {
- if (is_required && name === '--') {
- return;
- }
- if (use_buttons) {
- $select.insertAdjacentHTML('beforeend', `
- <div class="item">
- <input id="${$select.id}_${name}_radio" type="radio" name="${$select.id}"
- value="${name}" ${name === selected ? 'checked' : ''}>
- <label for="${$select.id}_${name}_radio">
- ${$select.id === 'bug_type' ? `
- <span class="bug-type-label iconic-text" data-type="${name}">
- <span class="icon" aria-hidden="true"></span>${name}
- </span>
- ` : `${name}`}
- </label>
- </div>
- `);
- } else {
- $select.insertAdjacentHTML('beforeend', `
- <option value="${name}" ${name === selected ? 'selected' : ''}>${name}</option>
- `);
- }
- });
- if ($select.matches('[multiple]') && value.length < 5) {
- $select.size = value.length;
- }
- });
+ try {
+ const data = await Bugzilla.API.get(`bug_modal/edit/${BUGZILLA.bug_id}`);
- // build our product description hash
- $.each(data.options.product, function() {
- products[this.name] = this.description;
- });
+ $('#io-error').empty().hide();
+ $('#mode-btn').hide();
- // keywords is a multi-value autocomplete
- keywords = data.keywords;
- $('#keywords')
- .devbridgeAutocomplete({
- appendTo: $('#main-inner'),
- forceFixPosition: true,
- lookup: function(query, done) {
- query = query.toLowerCase();
- var matchStart =
- $.grep(keywords, function(keyword) {
- return keyword.toLowerCase().substr(0, query.length) === query;
- });
- var matchSub =
- $.grep(keywords, function(keyword) {
- return keyword.toLowerCase().indexOf(query) !== -1 &&
- $.inArray(keyword, matchStart) === -1;
- });
- var suggestions =
- $.map($.merge(matchStart, matchSub), function(suggestion) {
- return { value: suggestion };
- });
- done({ suggestions: suggestions });
- },
- tabDisabled: true,
- delimiter: /,\s*/,
- minChars: 0,
- autoSelectFirst: false,
- triggerSelectOnValidInput: false,
- formatResult: function(suggestion, currentValue) {
- // disable <b> wrapping of matched substring
- return suggestion.value.htmlEncode();
- },
- onSearchStart: function(params) {
- var that = $(this);
- // adding spaces shouldn't initiate a new search
- var parts = that.val().split(/,\s*/);
- var query = parts[parts.length - 1];
- return query === $.trim(query);
- },
- onSelect: function() {
- this.value = this.value + ', ';
- this.focus();
- }
- })
- .addClass('bz_autocomplete');
-
- $('#cancel-btn').prop('disabled', false);
- $('#top-save-btn').show();
- $('#cancel-btn').show();
- $('#commit-btn').show();
- },
- function() {
- $('#mode-btn-readonly').show();
- $('#mode-btn-loading').hide();
- $('#mode-btn').prop('disabled', false);
- $('#mode-btn').show();
- $('#cancel-btn').hide();
- $('#commit-btn').hide();
-
- $('.edit-show').hide();
- $('.edit-hide').show();
+ // populate select menus
+ for (const [key, value] of Object.entries(data.options)) {
+ const $select = document.querySelector(`#${key}`);
+ if (!$select) {
+ continue;
+ }
+ // It can be radio-button-like UI
+ const use_buttons = $select.matches('.buttons.toggle');
+ const is_required = $select.matches('[aria-required="true"]');
+ const selected = use_buttons ? $select.querySelector('input').value : $select.value;
+ $select.innerHTML = '';
+ for (const { name } of value) {
+ if (is_required && name === '--') {
+ continue;
+ }
+ if (use_buttons) {
+ $select.insertAdjacentHTML('beforeend', `
+ <div class="item">
+ <input id="${$select.id}_${name}_radio" type="radio" name="${$select.id}"
+ value="${name}" ${name === selected ? 'checked' : ''}>
+ <label for="${$select.id}_${name}_radio">
+ ${$select.id === 'bug_type' ? `
+ <span class="bug-type-label iconic-text" data-type="${name}">
+ <span class="icon" aria-hidden="true"></span>${name}
+ </span>
+ ` : `${name}`}
+ </label>
+ </div>
+ `);
+ } else {
+ $select.insertAdjacentHTML('beforeend', `
+ <option value="${name}" ${name === selected ? 'selected' : ''}>${name}</option>
+ `);
+ }
+ }
+ if ($select.matches('[multiple]') && value.length < 5) {
+ $select.size = value.length;
+ }
}
- );
+
+ // build our product description hash
+ for (const { name, description } of data.options.product) {
+ products[name] = description;
+ }
+
+ // keywords is a multi-value autocomplete
+ keywords = data.keywords;
+ $('#keywords')
+ .devbridgeAutocomplete({
+ appendTo: $('#main-inner'),
+ forceFixPosition: true,
+ lookup: function(query, done) {
+ query = query.toLowerCase();
+ var matchStart =
+ $.grep(keywords, function(keyword) {
+ return keyword.toLowerCase().substr(0, query.length) === query;
+ });
+ var matchSub =
+ $.grep(keywords, function(keyword) {
+ return keyword.toLowerCase().indexOf(query) !== -1 &&
+ $.inArray(keyword, matchStart) === -1;
+ });
+ var suggestions =
+ $.map($.merge(matchStart, matchSub), function(suggestion) {
+ return { value: suggestion };
+ });
+ done({ suggestions });
+ },
+ tabDisabled: true,
+ delimiter: /,\s*/,
+ minChars: 0,
+ autoSelectFirst: false,
+ triggerSelectOnValidInput: false,
+ formatResult: function(suggestion, currentValue) {
+ // disable <b> wrapping of matched substring
+ return suggestion.value.htmlEncode();
+ },
+ onSearchStart: function(params) {
+ var that = $(this);
+ // adding spaces shouldn't initiate a new search
+ var parts = that.val().split(/,\s*/);
+ var query = parts[parts.length - 1];
+ return query === $.trim(query);
+ },
+ onSelect: function() {
+ this.value = this.value + ', ';
+ this.focus();
+ }
+ })
+ .addClass('bz_autocomplete');
+
+ $('#cancel-btn').prop('disabled', false);
+ $('#top-save-btn').show();
+ $('#cancel-btn').show();
+ $('#commit-btn').show();
+ } catch ({ message }) {
+ $('#io-error').html(message).show('fast');
+ $('#mode-btn-readonly').show();
+ $('#mode-btn-loading').hide();
+ $('#mode-btn').prop('disabled', false);
+ $('#mode-btn').show();
+ $('#cancel-btn').hide();
+ $('#commit-btn').hide();
+
+ $('.edit-show').hide();
+ $('.edit-hide').show();
+ }
});
$('#mode-btn').prop('disabled', false);
// cc toggle (follow/stop following)
$('#cc-btn')
- .click(function(event) {
+ .click(async event => {
event.preventDefault();
var is_cced = $(event.target).data('is-cced') == '1';
$('#add-self-cc').attr('disabled', false);
}
- bugzilla_ajax(
- {
- url: `${BUGZILLA.config.basepath}rest/bug/${BUGZILLA.bug_id}`,
- type: 'PUT',
- data: JSON.stringify({ cc: cc_change })
- },
- function(data) {
- $('#cc-btn').prop('disabled', false);
- if (!(data.bugs[0].changes && data.bugs[0].changes.cc))
- return;
- if (data.bugs[0].changes.cc.added == BUGZILLA.user.login) {
- $('#cc-btn')
- .text('Stop Following')
- .data('is-cced', '1');
- }
- else if (data.bugs[0].changes.cc.removed == BUGZILLA.user.login) {
- $('#cc-btn')
- .text('Follow')
- .data('is-cced', '0');
- }
- if ($('#cc-latch').data('expanded'))
- ccListUpdate();
- },
- function(message) {
- $('#cc-btn').prop('disabled', false);
- if ($('#cc-latch').data('expanded'))
- ccListUpdate();
+ try {
+ const { bugs } = await Bugzilla.API.put(`bug/${BUGZILLA.bug_id}`, { cc: cc_change });
+ const { changes } = bugs[0];
+
+ $('#io-error').empty().hide();
+ $('#cc-btn').prop('disabled', false);
+
+ if (!(changes && changes.cc)) {
+ return;
}
- );
+ if (changes.cc.added == BUGZILLA.user.login) {
+ $('#cc-btn').text('Stop Following').data('is-cced', '1');
+ } else if (changes.cc.removed == BUGZILLA.user.login) {
+ $('#cc-btn').text('Follow').data('is-cced', '0');
+ }
+
+ if ($('#cc-latch').data('expanded')) {
+ ccListUpdate();
+ }
+ } catch ({ message }) {
+ $('#io-error').html(message).show('fast');
+ $('#cc-btn').prop('disabled', false);
+
+ if ($('#cc-latch').data('expanded')) {
+ ccListUpdate();
+ }
+ }
});
// cancel button, reset the ui back to read-only state
var prefix = "(In reply to " + comment_author + " from comment #" + comment_no + ")\n";
var reply_text = "";
- var quoteMarkdown = function($comment) {
+ var quoteMarkdown = async $comment => {
const uid = $comment.data('comment-id');
- bugzilla_ajax(
- {
- url: `rest/bug/comment/${uid}`,
- },
- (data) => {
- const quoted = data['comments'][uid]['text'].replace(/\n/g, "\n> ");
- reply_text = `${prefix}> ${quoted}\n\n`;
- populateNewComment();
- }
- );
+
+ try {
+ const { comments } = await Bugzilla.API.get(`bug/comment/${uid}`, { include_fields: 'text' });
+ const quoted = comments[uid]['text'].replace(/\n/g, '\n> ');
+
+ reply_text = `${prefix}> ${quoted}\n\n`;
+ populateNewComment();
+ } catch (ex) {}
}
var populateNewComment = function() {
$(this).data('default', $(this).val());
});
$('#product')
- .change(function(event) {
+ .change(async event => {
$('#product-throbber').show();
$('#component, #version, #target_milestone').attr('disabled', true);
}
});
- bugzilla_ajax(
- {
- url: `${BUGZILLA.config.basepath}rest/bug_modal/new_product/${BUGZILLA.bug_id}?` +
- `product=${encodeURIComponent($('#product').val())}`
- },
- function(data) {
- $('#product-throbber').hide();
- $('#component, #version, #target_milestone').attr('disabled', false);
- var is_default = $('#product').val() == $('#product').data('default');
-
- // populate selects
- $.each(data, function(key, value) {
- if (key == 'groups') return;
- var el = $('#' + key);
- if (!el) return;
- el.empty();
- var selected = el.data('preselect');
- $(value).each(function(i, v) {
- el.append($('<option>', { value: v.name, text: v.name }));
- if (typeof selected === 'undefined' && v.selected)
- selected = v.name;
- });
- el.val(selected);
- el.prop('required', true);
- if (is_default) {
- el.removeClass('attention');
- el.val(el.data('default'));
- }
- else {
- el.addClass('attention');
- }
+ try {
+ const product = $('#product').val();
+ const data = await Bugzilla.API.get(`bug_modal/new_product/${BUGZILLA.bug_id}`, { product });
+
+ $('#io-error').empty().hide();
+ $('#product-throbber').hide();
+ $('#component, #version, #target_milestone').attr('disabled', false);
+ var is_default = $('#product').val() == $('#product').data('default');
+
+ // populate selects
+ $.each(data, function(key, value) {
+ if (key == 'groups') return;
+ var el = $('#' + key);
+ if (!el) return;
+ el.empty();
+ var selected = el.data('preselect');
+ $(value).each(function(i, v) {
+ el.append($('<option>', { value: v.name, text: v.name }));
+ if (typeof selected === 'undefined' && v.selected)
+ selected = v.name;
});
+ el.val(selected);
+ el.prop('required', true);
+ if (is_default) {
+ el.removeClass('attention');
+ el.val(el.data('default'));
+ }
+ else {
+ el.addClass('attention');
+ }
+ });
- // update groups
- var dirtyGroups = [];
- var any_groups_checked = 0;
+ // update groups
+ var dirtyGroups = [];
+ var any_groups_checked = 0;
+ $('#module-security').find('input[name=groups]').each(function() {
+ var that = $(this);
+ var defaultChecked = !!that.attr('checked');
+ if (defaultChecked !== that.is(':checked')) {
+ dirtyGroups.push({ name: that.val(), value: that.is(':checked') });
+ }
+ if (that.is(':checked')) {
+ any_groups_checked = 1;
+ }
+ });
+ $('#module-security .module-content')
+ .html(data.groups)
+ .addClass('attention');
+ $.each(dirtyGroups, function() {
+ $('#module-security').find('input[value=' + this.name + ']').prop('checked', this.value);
+ });
+ // clear any default groups if user was making bug public
+ // unless the group is mandatory for the new product
+ if (!any_groups_checked) {
$('#module-security').find('input[name=groups]').each(function() {
var that = $(this);
- var defaultChecked = !!that.attr('checked');
- if (defaultChecked !== that.is(':checked')) {
- dirtyGroups.push({ name: that.val(), value: that.is(':checked') });
+ if (!that.data('mandatory')) {
+ that.prop('checked', false);
}
- if (that.is(':checked')) {
- any_groups_checked = 1;
- }
- });
- $('#module-security .module-content')
- .html(data.groups)
- .addClass('attention');
- $.each(dirtyGroups, function() {
- $('#module-security').find('input[value=' + this.name + ']').prop('checked', this.value);
});
- // clear any default groups if user was making bug public
- // unless the group is mandatory for the new product
- if (!any_groups_checked) {
- $('#module-security').find('input[name=groups]').each(function() {
- var that = $(this);
- if (!that.data('mandatory')) {
- that.prop('checked', false);
- }
- });
- }
- },
- function() {
- $('#product-throbber').hide();
- $('#component, #version, #target_milestone').attr('disabled', false);
}
- );
+ } catch ({ message }) {
+ $('#io-error').html(message).show('fast');
+ $('#product-throbber').hide();
+ $('#component, #version, #target_milestone').attr('disabled', false);
+ }
});
// product/component search
// comment preview
var last_comment_text = '';
- $('#comment-tabs li').click(function() {
- var that = $(this);
+ $('#comment-tabs li').click(async event => {
+ var that = $(event.target);
if (that.attr('aria-selected') === 'true')
return;
return;
$('#preview-throbber').show();
preview.html('');
- bugzilla_ajax(
- {
- url: `${BUGZILLA.config.basepath}rest/bug/comment/render`,
- type: 'POST',
- data: { text: comment.val() },
- hideError: true
- },
- function(data) {
- $('#preview-throbber').hide();
- preview.html(data.html);
-
- // Highlight code if possible
- if (Prism) {
- Prism.highlightAllUnder(preview.get(0));
- }
- },
- function(message) {
- $('#preview-throbber').hide();
- var container = $('<div/>');
- container.addClass('preview-error');
- container.text(message);
- preview.html(container);
+
+ try {
+ const { html } = await Bugzilla.API.post('bug/comment/render', { text: comment.val() });
+
+ $('#preview-throbber').hide();
+ preview.html(html);
+
+ // Highlight code if possible
+ if (Prism) {
+ Prism.highlightAllUnder(preview.get(0));
}
- );
+ } catch ({ message }) {
+ $('#preview-throbber').hide();
+ var container = $('<div/>');
+ container.addClass('preview-error');
+ container.text(message);
+ preview.html(container);
+ }
+
last_comment_text = comment.val();
}).keydown(function(event) {
var that = $(this);
});
// Allow to attach pasted text directly
- document.querySelector('#comment').addEventListener('paste', event => {
+ document.querySelector('#comment').addEventListener('paste', async event => {
const text = event.clipboardData.getData('text');
const lines = text.split(/(?:\r\n|\r|\n)/).length;
event.preventDefault();
- bugzilla_ajax({
- type: 'POST',
- url: `/rest/bug/${BUGZILLA.bug_id}/attachment`,
- data: {
+ try {
+ await Bugzilla.API.post(`bug/${BUGZILLA.bug_id}/attachment`, {
// https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
data: btoa(encodeURIComponent(text).replace(/%([0-9A-F]{2})/g, (m, p1) => String.fromCharCode(`0x${p1}`))),
file_name: 'pasted.txt',
summary,
content_type: 'text/plain',
comment: event.target.value.trim(),
- },
- }, () => {
+ });
+
// Reload the page once upload is complete
location.replace(`${BUGZILLA.config.basepath}show_bug.cgi?id=${BUGZILLA.bug_id}`);
- }, message => {
+ } catch ({ message }) {
window.alert(`Couldn’t upload the text as an attachment. Please try again later. Error: ${message}`);
- });
+ }
});
restoreEditMode();
'The full URL is:\n\n' + url + '\n\nContinue?');
}
-function show_new_changes_indicator() {
- const url = `${BUGZILLA.config.basepath}rest/bug_user_last_visit/${BUGZILLA.bug_id}`;
+async function show_new_changes_indicator() {
+ const url = `bug_user_last_visit/${BUGZILLA.bug_id}`;
+
+ try {
+ // Get the last visited timestamp
+ const data = await Bugzilla.API.get(url);
- // Get the last visited timestamp
- bugzilla_ajax({ url }, data => {
// Save the current timestamp
- bugzilla_ajax({ url, type: 'POST' });
+ Bugzilla.API.post(url);
if (!data[0] || !data[0].last_visit_ts) {
return;
// TODO: Enable auto-scroll once the modal page layout is optimized
// scroll_element_into_view($separator);
- });
+ } catch (ex) {}
}
// fix url after bug creation/update
$('#ctag-error').show();
}
- function deleteTag(event) {
+ async function deleteTag(event) {
event.preventDefault();
$('#ctag-error').hide();
updateTagsMenu();
// update bugzilla
- bugzilla_ajax(
- {
- url: `${BUGZILLA.config.basepath}rest/bug/comment/${commentID}/tags`,
- type: 'PUT',
- data: { remove: [ tag ] },
- hideError: true
- },
- function(data) {
- renderTags(commentNo, data);
- updateTagsMenu();
- },
- function(message) {
- taggingError(commentNo, message);
- }
- );
+ try {
+ renderTags(commentNo, await Bugzilla.API.put(`bug/comment/${commentID}/tags`, { remove: [tag] }));
+ updateTagsMenu();
+ } catch ({ message }) {
+ taggingError(commentNo, message);
+ }
}
$('.comment-tag a').click(deleteTag);
$(`.comment[data-no="${commentNo}"]`).attr('data-tags', tags.join(' '));
}
- var refreshXHR;
+ let abort_controller;
- function refreshTags(commentNo, commentID) {
+ const refreshTags = async (commentNo, commentID) => {
cancelRefresh();
- refreshXHR = bugzilla_ajax(
- {
- url: `${BUGZILLA.config.basepath}rest/bug/comment/${commentID}?include_fields=tags`,
- hideError: true
- },
- function(data) {
- refreshXHR = false;
- renderTags(commentNo, data.comments[commentID].tags);
- },
- function(message) {
- refreshXHR = false;
+
+ try {
+ abort_controller = new AbortController();
+
+ const { signal } = abort_controller;
+ const { comments } = await Bugzilla.API.get(`bug/comment/${commentID}`, {
+ include_fields: ['tags'],
+ }, { signal });
+
+ renderTags(commentNo, comments[commentID].tags);
+ } catch ({ name, message }) {
+ if (name !== 'AbortError') {
taggingError(commentNo, message);
}
- );
+ } finally {
+ abort_controller = undefined;
+ }
}
function cancelRefresh() {
- if (refreshXHR) {
- refreshXHR.abort();
- refreshXHR = false;
+ if (abort_controller) {
+ abort_controller.abort();
+ abort_controller = undefined;
}
}
.devbridgeAutocomplete({
appendTo: $('#main-inner'),
forceFixPosition: true,
- serviceUrl: function(query) {
- return `${BUGZILLA.config.basepath}rest/bug/comment/tags/${encodeURIComponent(query)}`;
- },
- params: {
- Bugzilla_api_token: (BUGZILLA.api_token ? BUGZILLA.api_token : '')
- },
deferRequestBy: 250,
minChars: 3,
tabDisabled: true,
autoSelectFirst: true,
triggerSelectOnValidInput: false,
- transformResult: function(response) {
- response = $.parseJSON(response);
- return {
- suggestions: $.map(response, function(tag) {
- return { value: tag };
- })
- };
+ lookup: (query, done) => {
+ // Note: `async` doesn't work for this `lookup` function, so use a `Promise` chain instead
+ Bugzilla.API.get(`bug/comment/tags/${encodeURIComponent(query)}`)
+ .then(data => data.map(tag => ({ value: tag })))
+ .catch(() => [])
+ .then(suggestions => done({ suggestions }));
},
formatResult: function(suggestion, currentValue) {
// disable <b> wrapping of matched substring
return suggestion.value.htmlEncode();
}
})
- .keydown(function(event) {
+ .keydown(async event => {
if (event.which === 27) {
event.preventDefault();
$('#ctag-close').click();
renderTags(commentNo, tags);
// update bugzilla
- bugzilla_ajax(
- {
- url: `${BUGZILLA.config.basepath}rest/bug/comment/${commentID}/tags`,
- type: 'PUT',
- data: { add: addTags },
- hideError: true
- },
- function(data) {
- renderTags(commentNo, data);
- updateTagsMenu();
- },
- function(message) {
- taggingError(commentNo, message);
- refreshTags(commentNo, commentID);
- }
- );
+ try {
+ renderTags(commentNo, await Bugzilla.API.put(`bug/comment/${commentID}/tags`, { add: addTags }));
+ updateTagsMenu();
+ } catch ({ message }) {
+ taggingError(commentNo, message);
+ refreshTags(commentNo, commentID);
+ }
}
});
// Show text smaller than 50 KB
if (is_text && size < 50000) {
// Load text body
- bugzilla_ajax({ url: `${BUGZILLA.config.basepath}rest/bug/attachment/${id}?include_fields=data` }, data => {
- if (data.error) {
- return;
- }
-
- const text = decodeURIComponent(escape(atob(data.attachments[id].data)));
+ try {
+ const { attachments } = await Bugzilla.API.get(`bug/attachment/${id}`, { include_fields: 'data' });
+ const text = decodeURIComponent(escape(atob(attachments[id].data)));
const lang = is_patch ? 'diff' : type.match(/\w+$/)[0];
$att.insertAdjacentHTML('beforeend', `
Prism.highlightElement($att.querySelector('pre'));
$att.querySelectorAll('pre a').forEach($a => $a.tabIndex = -1);
}
- });
+ } catch (ex) {}
}
}
};
/**
* Implement the one-click Component Watching functionality that can be added to any page.
- * @abstract
*/
Bugzilla.ComponentWatching = class ComponentWatching {
/**
* Initialize a new ComponentWatching instance. Since constructors can't be async, use a separate function to move on.
*/
constructor() {
+ this._method = 'component-watching';
this.buttons = document.querySelectorAll('button.component-watching');
// Check if the user is logged in and the API key is available. If not, remove the Watch buttons.
}
/**
- * Send a REST API request, and return the results in a Promise.
- * @param {Object} [request={}] Request data. If omitted, all the current watches will be returned.
- * @param {String} [path=''] Optional path to be appended to the request URL.
- * @returns {Promise<Object|String>} Response data or error message.
+ * Retrieve the user's current watch list.
+ * @returns {Promise.<(Array.<Object>|Error)>} List of the current watches.
*/
- async fetch(request = {}, path = '') {
- request.url = `${BUGZILLA.config.basepath}rest/component-watching${path}`;
-
- return new Promise((resolve, reject) => bugzilla_ajax(request, data => resolve(data), error => reject(error)));
+ async list() {
+ return Bugzilla.API.get(this._method);
}
/**
* Start watching the current product or component.
* @param {String} product Product name.
* @param {String} [component=''] Component name. If omitted, all the components in the product will be watched.
- * @returns {Promise<Object|String>} Response data or error message.
+ * @returns {Promise.<(Object|Error)>} Detail of the added watch.
*/
async watch(product, component = '') {
- return this.fetch({ type: 'POST', data: { product, component } });
+ return Bugzilla.API.post(this._method, { product, component });
}
/**
* Stop watching the current product or component.
* @param {Number} id ID of the watch to be removed.
- * @returns {Promise<Object|String>} Response data or error message.
+ * @returns {Promise.<(Array.<Object>|Error)>} List of the current watches excluding the removed one.
*/
async unwatch(id) {
- return this.fetch({ type: 'DELETE' }, `/${id}`);
+ return Bugzilla.API.delete(`${this._method}/${id}`);
}
/**
*/
async init() {
try {
- const all_watches = await this.fetch();
+ const all_watches = await this.list();
this.get_buttons().forEach($button => {
const { product, component } = $button.dataset;
* Called whenever the Edit button is clicked. Hide the current comment and insert the inline comment editor instead.
* @param {MouseEvent} event Click event.
*/
- edit_button_onclick(event) {
+ async edit_button_onclick(event) {
event.preventDefault();
this.toggle_toolbar_buttons(true);
}
// Retrieve the raw comment text
- bugzilla_ajax({
- url: `${BUGZILLA.config.basepath}rest/editcomments/comment/${this.comment_id}`,
- hideError: true,
- }, data => {
- this.fetch_onload(data);
- }, message => {
+ try {
+ this.fetch_onload(await Bugzilla.API.get(`editcomments/comment/${this.comment_id}`));
+ } catch ({ message }) {
this.fetch_onerror(message);
- });
+ }
}
/**
/**
* Called whenever the Preview tab is clicked. Fetch and display the rendered comment.
*/
- preview() {
+ async preview() {
this.$preview.style.height = `${this.$textarea.scrollHeight}px`;
this.$edit_tab.setAttribute('aria-selected', 'false');
this.$edit_tabpanel.hidden = true;
this.render_message(this.str.loading);
- bugzilla_ajax({
- url: `${BUGZILLA.config.basepath}rest/bug/comment/render`,
- type: 'POST',
- hideError: true,
- data: { id: BUGZILLA.bug_id, text: this.$textarea.value },
- }, data => {
- this.$preview.innerHTML = data.html;
+ try {
+ const text = this.$textarea.value;
+ const { html } = await Bugzilla.API.post('bug/comment/render', { id: BUGZILLA.bug_id, text });
+
+ this.$preview.innerHTML = html;
// Highlight code if possible
if (Prism) {
this.$preview.style.removeProperty('height');
this.$preview.setAttribute('aria-busy', 'false');
- }, () => {
+ } catch (ex) {
this.render_message(this.str.preview_error);
this.$preview.setAttribute('aria-busy', 'false');
- });
+ }
}
/**
/**
* Called whenever the Update Comment button is clicked. Upload the changes to the server.
*/
- save() {
+ async save() {
if (!this.edited) {
return;
}
this.$textarea.disabled = this.$save_button.disabled = this.$cancel_button.disabled = true;
this.$status.textContent = this.str.saving;
- bugzilla_ajax({
- url: `${BUGZILLA.config.basepath}rest/editcomments/comment/${this.comment_id}`,
- type: 'PUT',
- hideError: true,
- data: {
+ try {
+ this.save_onsuccess(await Bugzilla.API.put(`editcomments/comment/${this.comment_id}`, {
new_comment: this.$textarea.value,
is_hidden: this.$is_hidden_checkbox && this.$is_hidden_checkbox.checked ? 1 : 0,
- },
- }, data => {
- this.save_onsuccess(data);
- }, message => {
+ }));
+ } catch ({ message }) {
this.save_onerror(message);
- });
+ }
}
/**
const change_when = $revision.dataset.revisedTime;
$checkbox.addEventListener('change', () => {
- bugzilla_ajax({
- url: `${BUGZILLA.config.basepath}rest/editcomments/revision`,
- type: 'PUT',
- data: {
- comment_id,
- change_when,
- is_hidden: $checkbox.checked ? 1 : 0,
- },
- });
+ Bugzilla.API.put('editcomments/revision', { comment_id, change_when, is_hidden: $checkbox.checked ? 1 : 0 });
});
}
};
* Called whenever a flag selection is changed. Insert or remove a comment template.
* @param {HTMLSelectElement} $select `<select>` element that the `change` event is fired.
*/
- flag_onselect($select) {
+ async flag_onselect($select) {
const id = Number($select.dataset.id);
const { name } = $select.dataset;
const state = $select.value;
this.$fieldset_wrapper.appendChild($fieldset);
// Show any other patches that can be requested for approval
- bugzilla_ajax({
- url: `${BUGZILLA.config.basepath}rest/bug/${this.bug_id}/attachment?` +
- 'include_fields=id,summary,content_type,is_patch,is_obsolete',
- }, ({ bugs }) => {
+ try {
+ const { bugs } = await Bugzilla.API.get(`bug/${this.bug_id}/attachment`, {
+ include_fields: ['id', 'summary', 'content_type', 'is_patch', 'is_obsolete'],
+ });
const attachments = bugs ? bugs[this.bug_id] : [];
const others = attachments.filter(att => att.id !== this.attachment_id && !att.is_obsolete &&
(att.is_patch || this.extra_patch_types.includes(att.content_type)));
</td></tr>
`);
}
- });
+ } catch (ex) {}
}
}
});
// Request approval for other patches if any
- await Promise.all([...this.inserted_fieldsets].map($fieldset => new Promise(resolve => {
+ await Promise.all([...this.inserted_fieldsets].map($fieldset => new Promise(async resolve => {
const ids = [...$fieldset.querySelectorAll('tr.other-patches input:checked')]
.map($input => Number($input.dataset.id));
const flags = this.find_selectors($fieldset).map($select => ({ name: $select.dataset.name, status: '?' }));
if (ids.length && flags.length) {
- bugzilla_ajax({
- type: 'PUT',
- url: `${BUGZILLA.config.basepath}rest/bug/attachment/${ids[0]}`,
- data: { ids, flags },
- }, () => resolve(), () => resolve());
- } else {
- resolve();
+ try {
+ await Bugzilla.API.put(`bug/attachment/${ids[0]}`, { ids, flags });
+ } catch (ex) {}
}
+
+ resolve();
})));
// Collect bug flags from checkboxes
// Update bug flags if needed
if (bug_flags.length) {
- await new Promise(resolve => {
- bugzilla_ajax({
- type: 'PUT',
- url: `${BUGZILLA.config.basepath}rest/bug/${this.bug_id}`,
- data: { flags: bug_flags },
- }, () => resolve(), () => resolve());
+ await new Promise(async resolve => {
+ try {
+ await Bugzilla.API.put(`bug/${this.bug_id}`, { flags: bug_flags });
+ } catch (ex) {}
+
+ resolve();
});
}
var product = {
details: false,
- _counter: 0,
_loaded: '',
_preselectedComponent: '',
return result;
},
- setName: function(productName) {
+ setName: async function(productName) {
if (productName == this.getName() && this.details)
return;
// grab the product information
this.details = false;
this._loaded = productName;
- YAHOO.util.Connect.setDefaultPostHeader('application/json; charset=UTF-8');
- YAHOO.util.Connect.asyncRequest(
- 'POST',
- `${BUGZILLA.config.basepath}jsonrpc.cgi`,
- {
- success: function(res) {
- try {
- var data = JSON.parse(res.responseText);
- if (data.error)
- throw(data.error.message);
- if (data.result.products.length == 0)
- document.location.href = `${BUGZILLA.config.basepath}enter_bug.cgi?format=guided`;
- product.details = data.result.products[0];
- bugForm.onProductUpdated();
- } catch (err) {
- product.details = false;
- bugForm.onProductUpdated();
- if (err) {
- alert('Failed to retrieve components for product "' +
- productName + '":' + "\n\n" + err);
- if (console)
- console.error(err);
- }
- }
- },
- failure: function(res) {
- this._loaded = '';
- product.details = false;
- bugForm.onProductUpdated();
- if (res.responseText) {
- alert('Failed to retrieve components for product "' +
- productName + '":' + "\n\n" + res.responseText);
- if (console)
- console.error(res);
- }
- }
- },
- JSON.stringify({
- version: "1.1",
- method: "Product.get",
- id: ++this._counter,
- params: {
- names: [productName],
- exclude_fields: ['internals', 'milestones', 'components.flag_types'],
- Bugzilla_api_token : (BUGZILLA.api_token ? BUGZILLA.api_token : '')
- }
+
+ try {
+ const { products } = await Bugzilla.API.get('product', {
+ names: [productName],
+ exclude_fields: ['internals', 'milestones', 'components.flag_types'],
+ });
+
+ if (products.length) {
+ product.details = products[0];
+ bugForm.onProductUpdated();
+ } else {
+ document.location.href = `${BUGZILLA.config.basepath}enter_bug.cgi?format=guided`;
}
- )
- );
+ } catch ({ message }) {
+ this._loaded = '';
+ product.details = false;
+ bugForm.onProductUpdated();
+ alert(`Failed to retrieve components for product "${productName}"\n\n${message}`);
+ }
}
};
// duplicates step
var dupes = {
- _counter: 0,
_dataTable: null,
_dataTableColumns: null,
_elSummary: null,
},
_initDataTable: function() {
- var dataSource = new YAHOO.util.XHRDataSource(`${BUGZILLA.config.basepath}jsonrpc.cgi`);
- dataSource.connTimeout = 15000;
- dataSource.connMethodPost = true;
- dataSource.connXhrMode = "cancelStaleRequests";
- dataSource.maxCacheEntries = 3;
- dataSource.responseSchema = {
- resultsList : "result.bugs",
- metaFields : { error: "error", jsonRpcId: "id" }
- };
- // DataSource can't understand a JSON-RPC error response, so
- // we have to modify the result data if we get one.
- dataSource.doBeforeParseData =
- function(oRequest, oFullResponse, oCallback) {
- if (oFullResponse.error) {
- oFullResponse.result = {};
- oFullResponse.result.bugs = [];
- if (console)
- console.error("JSON-RPC error:", oFullResponse.error);
- }
- return oFullResponse;
- };
- dataSource.subscribe('dataErrorEvent',
- function() {
- dupes._currentSearchQuery = '';
- }
- );
-
this._dataTable = new YAHOO.widget.DataTable(
'dupes_list',
this._dataTableColumns,
- dataSource,
+ new YAHOO.util.LocalDataSource([]), // Dummy data source
{
initialLoad: false,
MSG_EMPTY: 'No similar issues found.',
el.appendChild(button);
},
- updateFollowing: function(el, bugID, bugStatus, button, follow) {
+ updateFollowing: async function(el, bugID, bugStatus, button, follow) {
button.disabled = true;
button.innerHTML = 'Updating...';
ccObject = { remove: [ guided.currentUser ] };
}
- YAHOO.util.Connect.setDefaultPostHeader('application/json; charset=UTF-8');
- YAHOO.util.Connect.asyncRequest(
- 'POST',
- `${BUGZILLA.config.basepath}jsonrpc.cgi`,
- {
- success: function(res) {
- var data = JSON.parse(res.responseText);
- if (data.error)
- throw(data.error.message);
- dupes._buildCcHTML(el, bugID, bugStatus, follow);
- },
- failure: function(res) {
- dupes._buildCcHTML(el, bugID, bugStatus, !follow);
- if (res.responseText)
- alert("Update failed:\n\n" + res.responseText);
- }
- },
- JSON.stringify({
- version: "1.1",
- method: "Bug.update",
- id: ++this._counter,
- params: {
- ids: [ bugID ],
- cc : ccObject,
- Bugzilla_api_token: (BUGZILLA.api_token ? BUGZILLA.api_token : '')
- }
- })
- );
+ try {
+ await Bugzilla.API.put(`bug/${bugID}`, { ids: [bugID], cc: ccObject });
+ dupes._buildCcHTML(el, bugID, bugStatus, follow);
+ } catch ({ message }) {
+ dupes._buildCcHTML(el, bugID, bugStatus, !follow);
+ alert(`Update failed:\n\n${message}`);
+ }
},
reset: function() {
dupes._elSearch.disabled = dupes._elSummary.value.trim() == '';
},
- _doSearch: function() {
+ _doSearch: async function() {
if (dupes.getSummary().length < 4) {
alert('The summary must be at least 4 characters long.');
return;
' width="16" height="11">',
YAHOO.widget.DataTable.CLASS_LOADING
);
- var json_object = {
- version: "1.1",
- method: "Bug.possible_duplicates",
- id: ++dupes._counter,
- params: {
- product: product._getNameAndRelated(),
- summary: dupes.getSummary(),
- limit: 12,
- include_fields: [ "id", "summary", "status", "resolution",
- "update_token", "cc", "component" ],
- Bugzilla_api_token: (BUGZILLA.api_token ? BUGZILLA.api_token : '')
- }
- };
-
- dupes._dataTable.getDataSource().sendRequest(
- JSON.stringify(json_object),
- {
- success: dupes._onDupeResults,
- failure: dupes._onDupeResults,
- scope: dupes._dataTable,
- argument: dupes._dataTable.getState()
- }
- );
Dom.get('dupes_continue_button_top').disabled = true;
Dom.get('dupes_continue_button_bottom').disabled = true;
Dom.removeClass('dupes_continue', 'hidden');
+
+ let data;
+
+ try {
+ const { bugs } = await Bugzilla.API.get('bug/possible_duplicates', {
+ product: product._getNameAndRelated(),
+ summary: dupes.getSummary(),
+ limit: 12,
+ include_fields: ['id', 'summary', 'status', 'resolution', 'update_token', 'cc', 'component'],
+ });
+
+ data = { results: bugs };
+ } catch (ex) {
+ dupes._currentSearchQuery = '';
+ data = { error: true };
+ }
+
+ Dom.removeClass('advanced', 'hidden');
+ Dom.removeClass('dupes_continue_button_top', 'hidden');
+ Dom.get('dupes_continue_button_top').disabled = false;
+ Dom.get('dupes_continue_button_bottom').disabled = false;
+ dupes._dataTable.onDataReturnInitializeTable('', data);
} catch(err) {
if (console)
console.error(err.message);
}
},
- _onDupeResults: function(sRequest, oResponse, oPayload) {
- Dom.removeClass('advanced', 'hidden');
- Dom.removeClass('dupes_continue_button_top', 'hidden');
- Dom.get('dupes_continue_button_top').disabled = false;
- Dom.get('dupes_continue_button_bottom').disabled = false;
- dupes._dataTable.onDataReturnInitializeTable(sRequest, oResponse,
- oPayload);
- },
-
getSummary: function() {
var summary = this._elSummary.value.trim();
// work around chrome bug
}
sub rest_resources {
- return [qr{^/bug_interest_unmark$}, {PUT => {method => 'bug_interest_unmark'}}];
+ return [
+ # REST API v1: Expose `bug_interest_unmark` method without prefix for backward compatibility
+ qr{^/(mydashboard/)?bug_interest_unmark$}, {PUT => {method => 'bug_interest_unmark'}},
+ # Other methods are new, so require the prefix
+ qr{^/mydashboard/run_bug_query$}, {GET => {method => 'run_bug_query'}},
+ qr{^/mydashboard/run_flag_query$}, {GET => {method => 'run_flag_query'}},
+ qr{^/mydashboard/run_last_changes$}, {GET => {method => 'run_last_changes'}},
+ ];
}
1;
YUI({
base: 'js/yui3/',
combine: false
- }).use("node", "datatable", "datatable-sort",
- "datatable-datasource", "datasource-io", "datasource-jsonschema", function(Y) {
+ }).use('node', 'datatable', 'datatable-sort', 'escape', function(Y) {
// Common
- var counter = 0;
- var dataSource = {
- requestee: null,
- requester: null
- };
var dataTable = {
requestee: null,
requester: null
};
- var updateFlagTable = function(type) {
+ var updateFlagTable = async type => {
if (!type) return;
- counter = counter + 1;
-
- var callback = {
- success: function(e) {
- if (e.response) {
- Y.one('#' + type + '_loading').addClass('bz_default_hidden');
- Y.one('#' + type + '_count_refresh').removeClass('bz_default_hidden');
- Y.one("#" + type + "_flags_found").setHTML(
- e.response.results.length +
- ' request' + (e.response.results.length == 1 ? '' : 's') +
- ' found');
- dataTable[type].set('data', e.response.results);
- }
- },
- failure: function(o) {
- Y.one('#' + type + '_loading').addClass('bz_default_hidden');
- Y.one('#' + type + '_count_refresh').removeClass('bz_default_hidden');
- if (o.error && o.error.message) {
- alert("Failed to load requests:\n\n" + o.error.message);
- } else {
- alert("Failed to load requests");
- }
- }
- };
-
- var json_object = {
- version: "1.1",
- method: "MyDashboard.run_flag_query",
- id: counter,
- params: {
- type : type,
- Bugzilla_api_token : (BUGZILLA.api_token ? BUGZILLA.api_token : '')
- }
- };
-
- var stringified = JSON.stringify(json_object);
-
Y.one('#' + type + '_loading').removeClass('bz_default_hidden');
Y.one('#' + type + '_count_refresh').addClass('bz_default_hidden');
dataTable[type].render("#" + type + "_table");
dataTable[type].showMessage('loadingMessage');
- dataSource[type].sendRequest({
- request: stringified,
- cfg: {
- method: "POST",
- headers: { 'Content-Type': 'application/json' }
- },
- callback: callback
- });
+ try {
+ const { result } = await Bugzilla.API.get('mydashboard/run_flag_query', { type });
+ const results = result[type];
+
+ Y.one(`#${type}_loading`).addClass('bz_default_hidden');
+ Y.one(`#${type}_count_refresh`).removeClass('bz_default_hidden');
+ Y.one(`#${type}_flags_found`)
+ .setHTML(`${results.length} ${(results.length === 1 ? 'request' : 'requests')} found`);
+
+ dataTable[type].set('data', results);
+ dataTable[type].render(`#${type}_table`);
+ } catch ({ message }) {
+ Y.one(`#${type}_loading`).addClass('bz_default_hidden');
+ Y.one(`#${type}_count_refresh`).removeClass('bz_default_hidden');
+
+ alert(`Failed to load requests:\n\n${message}`);
+ }
};
var loadBugList = function(type) {
};
// Requestee
- dataSource.requestee = new Y.DataSource.IO({ source: `${BUGZILLA.config.basepath}jsonrpc.cgi` });
- dataSource.requestee.on('error', function(e) {
- try {
- var response = JSON.parse(e.data.responseText);
- if (response.error)
- e.error.message = response.error.message;
- } catch(ex) {
- // ignore
- }
- });
dataTable.requestee = new Y.DataTable({
columns: [
{ key: "requester", label: "Requester", sortable: true },
dataTable.requestee.plug(Y.Plugin.DataTableSort);
- dataTable.requestee.plug(Y.Plugin.DataTableDataSource, {
- datasource: dataSource.requestee
- });
-
- dataSource.requestee.plug(Y.Plugin.DataSourceJSONSchema, {
- schema: {
- resultListLocator: "result.result.requestee",
- resultFields: ["requester", "type", "attach_id", "is_patch", "bug_id",
- "bug_status", "bug_summary", "updated", "updated_fancy"]
- }
- });
-
- dataTable.requestee.render("#requestee_table");
-
Y.one('#requestee_refresh').on('click', function(e) {
updateFlagTable('requestee');
});
});
// Requester
- dataSource.requester = new Y.DataSource.IO({ source: `${BUGZILLA.config.basepath}jsonrpc.cgi` });
- dataSource.requester.on('error', function(e) {
- try {
- var response = JSON.parse(e.data.responseText);
- if (response.error)
- e.error.message = response.error.message;
- } catch(ex) {
- // ignore
- }
- });
dataTable.requester = new Y.DataTable({
columns: [
{ key:"requestee", label:"Requestee", sortable:true,
dataTable.requester.plug(Y.Plugin.DataTableSort);
- dataTable.requester.plug(Y.Plugin.DataTableDataSource, {
- datasource: dataSource.requester
- });
-
- dataSource.requester.plug(Y.Plugin.DataSourceJSONSchema, {
- schema: {
- resultListLocator: "result.result.requester",
- resultFields: ["requestee", "type", "attach_id", "is_patch", "bug_id",
- "bug_status", "bug_summary", "updated", "updated_fancy"]
- }
- });
-
Y.one('#requester_refresh').on('click', function(e) {
updateFlagTable('requester');
});
patterns: { 'gallery-': {} }
}
}
- }).use("node", "datatable", "datatable-sort", "datatable-message",
- "datatable-datasource", "datasource-io", "datasource-jsonschema", "cookie",
+ }).use("node", "datatable", "datatable-sort", "datatable-message", "cookie",
"gallery-datatable-row-expansion-bmo", "handlebars", function(Y) {
- var counter = 0,
- bugQueryTable = null,
- bugQuery = null,
- lastChangesQuery = null,
+ var bugQueryTable = null,
lastChangesCache = {},
default_query = "assignedbugs";
}
}
- var bugQuery = new Y.DataSource.IO({ source: `${BUGZILLA.config.basepath}jsonrpc.cgi` });
-
- bugQuery.plug(Y.Plugin.DataSourceJSONSchema, {
- schema: {
- resultListLocator: "result.result.bugs",
- resultFields: ["bug_id", "bug_type", "changeddate", "changeddate_fancy",
- "bug_status", "short_desc", "changeddate_api" ],
- metaFields: {
- description: "result.result.description",
- heading: "result.result.heading",
- buffer: "result.result.buffer",
- mark_read: "result.result.mark_read"
- }
- }
- });
-
- bugQuery.on('error', function(e) {
- try {
- var response = JSON.parse(e.data.responseText);
- if (response.error)
- e.error.message = response.error.message;
- } catch(ex) {
- // ignore
- }
- });
-
- var bugQueryCallback = {
- success: function(e) {
- if (e.response) {
- Y.one('#query_loading').addClass('bz_default_hidden');
- Y.one('#query_count_refresh').removeClass('bz_default_hidden');
- Y.one("#query_container .query_description").setHTML(e.response.meta.description);
- Y.one("#query_container .query_heading").setHTML(e.response.meta.heading);
- Y.one("#query_bugs_found").setHTML(
- `<a href="${BUGZILLA.config.basepath}buglist.cgi?${e.response.meta.buffer}" target="_blank">` +
- `${e.response.results.length} bugs found</a>`);
- bugQueryTable.set('data', e.response.results);
-
- var mark_read = e.response.meta.mark_read;
- if (mark_read) {
- Y.one('#query_markread').setHTML( mark_read );
- Y.one('#bar_markread').removeClass('bz_default_hidden');
- Y.one('#query_markread_text').setHTML( mark_read );
- Y.one('#query_markread').removeClass('bz_default_hidden');
- }
- else {
- Y.one('#bar_markread').addClass('bz_default_hidden');
- Y.one('#query_markread').addClass('bz_default_hidden');
- }
- Y.one('#query_markread_text').addClass('bz_default_hidden');
- }
- },
- failure: function(o) {
- Y.one('#query_loading').addClass('bz_default_hidden');
- Y.one('#query_count_refresh').removeClass('bz_default_hidden');
- if (o.error) {
- alert("Failed to load bug list from Bugzilla:\n\n" + o.error.message);
- } else {
- alert("Failed to load bug list from Bugzilla.");
- }
- }
- };
-
- var updateQueryTable = function(query_name) {
+ var updateQueryTable = async query_name => {
if (!query_name) return;
- counter = counter + 1;
lastChangesCache = {};
Y.one('#query_loading').removeClass('bz_default_hidden');
bugQueryTable.render("#query_table");
bugQueryTable.showMessage('loadingMessage');
- var bugQueryParams = {
- version: "1.1",
- method: "MyDashboard.run_bug_query",
- id: counter,
- params: { query : query_name,
- Bugzilla_api_token : (BUGZILLA.api_token ? BUGZILLA.api_token : '')
+ try {
+ const { result } = await Bugzilla.API.get('mydashboard/run_bug_query', { query: query_name });
+ const { buffer, bugs, description, heading, mark_read } = result;
+
+ Y.one('#query_loading').addClass('bz_default_hidden');
+ Y.one('#query_count_refresh').removeClass('bz_default_hidden');
+ Y.one("#query_container .query_description").setHTML(description);
+ Y.one("#query_container .query_heading").setHTML(heading);
+ Y.one("#query_bugs_found").setHTML(`<a href="${BUGZILLA.config.basepath}buglist.cgi?${buffer}" ` +
+ `target="_blank">${bugs.length} bugs found</a>`);
+
+ bugQueryTable.set('data', bugs);
+ bugQueryTable.render("#query_table");
+
+ if (mark_read) {
+ Y.one('#query_markread').setHTML(mark_read);
+ Y.one('#bar_markread').removeClass('bz_default_hidden');
+ Y.one('#query_markread_text').setHTML(mark_read);
+ Y.one('#query_markread').removeClass('bz_default_hidden');
+ } else {
+ Y.one('#bar_markread').addClass('bz_default_hidden');
+ Y.one('#query_markread').addClass('bz_default_hidden');
}
- };
- bugQuery.sendRequest({
- request: JSON.stringify(bugQueryParams),
- cfg: {
- method: "POST",
- headers: { 'Content-Type': 'application/json' }
- },
- callback: bugQueryCallback
- });
+ Y.one('#query_markread_text').addClass('bz_default_hidden');
+ } catch ({ message }) {
+ Y.one('#query_loading').addClass('bz_default_hidden');
+ Y.one('#query_count_refresh').removeClass('bz_default_hidden');
+
+ alert(`Failed to load bug list from Bugzilla:\n\n${message}`);
+ }
};
var updatedFormatter = function(o) {
`<a href="${BUGZILLA.config.basepath}show_bug.cgi?id=${data.bug_id}" target="_blank">
${isNaN(value) ? value.htmlEncode().wbr() : value}</a>`;
- lastChangesQuery = new Y.DataSource.IO({ source: `${BUGZILLA.config.basepath}jsonrpc.cgi` });
-
- lastChangesQuery.plug(Y.Plugin.DataSourceJSONSchema, {
- schema: {
- resultListLocator: "result.results",
- resultFields: ["last_changes"],
- }
- });
-
- lastChangesQuery.on('error', function(e) {
- try {
- var response = JSON.parse(e.data.responseText);
- if (response.error)
- e.error.message = response.error.message;
- } catch(ex) {
- // ignore
- }
- });
-
bugQueryTable = new Y.DataTable({
columns: [
{ key: Y.Plugin.DataTableRowExpansion.column_key, label: ' ', sortable: false },
bugQueryTable.plug(Y.Plugin.DataTableRowExpansion, {
uniqueIdKey: 'bug_id',
- template: function(data) {
- var bug_id = data.bug_id;
+ template: ({ bug_id, changeddate_api }) => {
+ // Note: `async` doesn't work for this `template` function, so use an inner `async` function instead
+ if (!lastChangesCache[bug_id]) {
+ try {
+ (async () => {
+ const { results } =
+ await Bugzilla.API.get('mydashboard/run_last_changes', { bug_id, changeddate_api });
+ const { last_changes } = results[0];
- var lastChangesCallback = {
- success: function(e) {
- if (e.response) {
- var last_changes = e.response.results[0].last_changes;
last_changes['bug_id'] = bug_id;
lastChangesCache[bug_id] = last_changes;
Y.one('#last_changes_stub_' + bug_id).setHTML(last_changes_template(last_changes));
- }
- },
- failure: function(o) {
- if (o.error) {
- alert("Failed to load last changes from Bugzilla:\n\n" + o.error.message);
- } else {
- alert("Failed to load last changes from Bugzilla.");
- }
+ })();
+ } catch ({ message }) {
+ alert(`Failed to load last changes from Bugzilla:\n\n${message}`);
}
- };
-
- if (!lastChangesCache[bug_id]) {
- var lastChangesParams = {
- version: "1.1",
- method: "MyDashboard.run_last_changes",
- params: {
- bug_id: data.bug_id,
- changeddate_api: data.changeddate_api,
- Bugzilla_api_token : (BUGZILLA.api_token ? BUGZILLA.api_token : '')
- }
- };
-
- lastChangesQuery.sendRequest({
- request: JSON.stringify(lastChangesParams),
- cfg: {
- method: "POST",
- headers: { 'Content-Type': 'application/json' }
- },
- callback: lastChangesCallback
- });
return stub_template({bug_id: bug_id});
}
bugQueryTable.plug(Y.Plugin.DataTableSort);
- bugQueryTable.plug(Y.Plugin.DataTableDataSource, {
- datasource: bugQuery
- });
-
// Initial load
Y.on("contentready", function (e) {
updateQueryTable(default_query);
for (var i = 0, l = data.size(); i < l; i++) {
bug_ids.push(data.item(i).get('bug_id'));
}
- YAHOO.bugzilla.bugUserLastVisit.update(bug_ids);
- YAHOO.bugzilla.bugInterest.unmark(bug_ids);
+ Bugzilla.API.post('bug_user_last_visit', { ids: bug_ids });
+ Bugzilla.API.put('mydashboard/bug_interest_unmark', { bug_ids });
});
Y.one('#query_buglist').on('click', function(e) {
var Phabricator = {};
-Phabricator.getBugRevisions = function() {
+Phabricator.getBugRevisions = async () => {
var phabUrl = $('.phabricator-revisions').data('phabricator-base-uri');
var tr = $('<tr/>');
var td = $('<td/>');
errRow.removeClass('bz_default_hidden');
}
- var $getUrl = '/rest/phabbugz/bug_revisions/' + BUGZILLA.bug_id +
- '?Bugzilla_api_token=' + BUGZILLA.api_token;
+ try {
+ const { revisions } = await Bugzilla.API.get(`phabbugz/bug_revisions/${BUGZILLA.bug_id}`);
- $.getJSON($getUrl, function(data) {
- if (data.revisions.length === 0) {
- displayLoadError('none returned from server');
- } else {
- var i = 0;
- for (; i < data.revisions.length; i++) {
- tbody.append(revisionRow(data.revisions[i]));
- }
- }
- tbody.find('.phabricator-loading-row').addClass('bz_default_hidden');
- }).fail(function(jqXHR, textStatus, errorThrown) {
- var errStr;
- if (jqXHR.responseJSON && jqXHR.responseJSON.err &&
- jqXHR.responseJSON.err.msg) {
- errStr = jqXHR.responseJSON.err.msg;
- } else if (errorThrown) {
- errStr = errorThrown;
+ if (revisions.length) {
+ revisions.forEach(rev => tbody.append(revisionRow(rev)));
} else {
- errStr = 'unknown';
+ displayLoadError('none returned from server');
}
- displayLoadError(errStr);
- tbody.find('.phabricator-loading-row').addClass('bz_default_hidden');
- });
+ } catch ({ message }) {
+ displayLoadError(message);
+ }
+
+ tbody.find('.phabricator-loading-row').addClass('bz_default_hidden');
};
$().ready(function() {
$('.prod_comp_search')
.each(function() {
var that = $(this);
- var params = {
- limit: (that.data('max_results') + 1)
- };
- if (BUGZILLA.api_token) {
- params.Bugzilla_api_token = BUGZILLA.api_token;
- }
+ const limit = that.data('max_results') + 1;
+
that.devbridgeAutocomplete({
appendTo: $('#main-inner'),
forceFixPosition: true,
- serviceUrl: function(query) {
- return `${BUGZILLA.config.basepath}rest/prod_comp_search/find/${encodeURIComponent(query)}`;
- },
- params: params,
deferRequestBy: 250,
minChars: 3,
maxHeight: 500,
autoSelectFirst: true,
triggerSelectOnValidInput: false,
width: '',
- transformResult: function(response) {
- response = $.parseJSON(response);
- if (response.error) {
- that.data('error', response.message);
- return { suggestions: [] };
- }
- return {
- suggestions: $.map(response.products, function(dataItem) {
- if (dataItem.component) {
- return {
- value: dataItem.product + ' :: ' + dataItem.component,
- data : dataItem
- };
- }
- else {
- return {
- value: dataItem.product,
- data : dataItem
- };
- }
- })
- };
+ lookup: (query, done) => {
+ // Note: `async` doesn't work for this `lookup` function, so use a `Promise` chain instead
+ Bugzilla.API.get(`prod_comp_search/find/${encodeURIComponent(query)}`, { limit })
+ .then(({ products }) => products.map(item => ({
+ value: `${item.product}${item.component ? ` :: ${item.component}` : ''}`,
+ data : item,
+ })))
+ .catch(() => [])
+ .then(suggestions => done({ suggestions }));
},
formatResult: function(suggestion, currentValue) {
var value = (suggestion.data.component ? suggestion.data.component : suggestion.data.product);
* @returns {Promise} Results or error.
*/
async fetch() {
- return new Promise((resolve, reject) => bugzilla_ajax({
- url: `${BUGZILLA.config.basepath}rest/prod_comp_search/frequent`
- }, ({ results }) => {
+ try {
+ const { results } = await Bugzilla.API.get('prod_comp_search/frequent');
+
if (!results) {
- reject(new Error('Your frequent components could not be retrieved.'));
- } else if (!results.length) {
- reject(new Error(('Your frequent components could not be found.')));
- } else {
- resolve(results);
+ return Promise.reject(new Error('Your frequent components could not be retrieved.'));
}
- }, () => {
- reject(new Error('Your frequent components could not be retrieved.'));
- }));
+
+ if (!results.length) {
+ return Promise.reject(new Error('Your frequent components could not be found.'));
+ }
+
+ return Promise.resolve(results);
+ } catch (ex) {
+ return Promise.reject(new Error('Your frequent components could not be retrieved.'));
+ }
}
};
return true;
}
-function getBugInfo (evt) {
+async function getBugInfo (evt) {
var bug_id = parseInt(this.value);
var div = $("#bug_info");
div.text('Getting bug info...');
- var url = `${BUGZILLA.config.basepath}rest/bug/${bug_id}?` +
- `include_fields=product,component,status,summary&Bugzilla_api_token=${BUGZILLA.api_token}`;
- $.getJSON(url).done(function(data) {
- var bug_message = "";
- if (data) {
- if (data.bugs[0].product !== 'Mozilla Reps'
- || data.bugs[0].component !== 'Budget Requests')
- {
- bug_message = "You can only attach budget payment " +
- "information to bugs under the product " +
- "'Mozilla Reps' and component 'Budget Requests'.";
- }
- else {
- bug_message = "Bug " + bug_id + " - " + data.bugs[0].status +
- " - " + data.bugs[0].summary;
- }
- }
- else {
- bug_message = "Get bug failed: " + data.responseText;
- }
+ try {
+ const { bugs } = await Bugzilla.API.get(`bug/${bug_id}?`, {
+ include_fields: ['product', 'component', 'status', 'summary'],
+ });
+ const { product, component, status, summary } = bugs[0];
+ const bug_message = (product !== 'Mozilla Reps' || component !== 'Budget Requests') ?
+ "You can only attach budget payment information to bugs under \
+ the product 'Mozilla Reps' and component 'Budget Requests'." :
+ `Bug ${bug_id} - ${status} - ${summary}`;
+
div.text(bug_message);
bug_cache[bug_id] = bug_message;
- }).fail(function(res, x, y) {
- if (res.responseJSON && res.responseJSON.error) {
- div.text(res.responseJSON.message);
- }
- });
+ } catch ({ message }) {
+ div.text(`Get bug failed: ${message}`);
+ }
+
return true;
}
return new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5]);
}
- var flagDS, bugDS, attachmentDS, historyTable;
- flagDS = new Y.DataSource.IO({ source: `${BUGZILLA.config.basepath}jsonrpc.cgi` });
- flagDS.plug(Y.Plugin.DataSourceJSONSchema, {
- schema: {
- resultListLocator: 'result',
- resultFields: [
- { key: 'id' },
- { key: 'requestee' },
- { key: 'setter' },
- { key: 'flag_id' },
- { key: 'creation_time' },
- { key: 'status' },
- { key: 'bug_id' },
- { key: 'type' },
- { key: 'attachment_id' }
- ]
- }
- });
-
- bugDS = new Y.DataSource.IO({ source: `${BUGZILLA.config.basepath}jsonrpc.cgi` });
- bugDS.plug(Y.Plugin.DataSourceJSONSchema, {
- schema: {
- resultListLocator: 'result.bugs',
- resultFields: [
- { key: 'id' },
- { key: 'summary' }
- ]
- }
- });
-
- attachmentDS = new Y.DataSource.IO({ source: `${BUGZILLA.config.basepath}jsonrpc.cgi` });
- attachmentDS.plug(Y.Plugin.DataSourceJSONSchema, {
- schema: {
- metaFields: { 'attachments': 'result.attachments' }
- }
- });
-
- historyTable = new Y.DataTable({
+ const historyTable = new Y.DataTable({
columns: [
{ key: 'creation_time', label: 'Created', sortable: true, formatter: format_date },
{ key: 'attachment', label: 'Attachment', formatter: format_attachment },
]
});
- function fetch_flag_ids(user) {
- return new Y.Promise(function (resolve, reject) {
- var flagIdCallback = {
- success: function (e) {
- var flags = e.response.results;
- var flag_ids = flags.filter(function (flag) {
- return flag.status == '?';
- })
- .map(function (flag) {
- return flag.flag_id;
- });
-
- if (flag_ids.length > 0) {
- resolve(flag_ids);
- } else {
- reject("No reviews found");
- }
- },
- failure: function (e) {
- reject(e.error.message);
- }
- };
-
- flagDS.sendRequest({
- request: JSON.stringify({
- version: '1.1',
- method: 'Review.flag_activity',
- params: {
- type_name: 'review',
- requestee: user,
- include_fields: ['flag_id', 'status'],
- Bugzilla_api_token : (BUGZILLA.api_token ? BUGZILLA.api_token : '')
- }
- }),
- cfg: {
- method: "POST",
- headers: { 'Content-Type': 'application/json' }
- },
- callback: flagIdCallback
- });
- });
- }
+ const fetch_flags = async user => {
+ try {
+ const data = await Bugzilla.API.get('review/flag_activity', { type_name: 'review', requestee: user });
+ const flags = data.filter(flag => flag.status === '?');
- function fetch_flags(flag_ids) {
- return new Y.Promise(function (resolve, reject) {
- flagDS.sendRequest({
- request: JSON.stringify({
- version: '1.1',
- method: 'Review.flag_activity',
- params: {
- flag_ids: flag_ids,
- Bugzilla_api_token: (BUGZILLA.api_token ? BUGZILLA.api_token : '')
- }
- }),
- cfg: {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' }
- },
- callback: {
- success: function (e) {
- var flags = e.response.results;
- flags.forEach(function(flag) {
- flag.creation_time = parse_date(flag.creation_time);
- });
- resolve(flags.sort(function (a, b) {
- if (a.id > b.id) return 1;
- if (a.id < b.id) return -1;
- return 0;
- }));
- },
- failure: function (e) {
- reject(e.error.message);
- }
- }
- });
- });
+ if (!flags.length) {
+ return Promise.reject("No reviews found");
+ }
+
+ flags.forEach(flag => flag.creation_time = parse_date(flag.creation_time));
+ flags.sort((a, b) => a.id > b.id ? 1 : a.id < b.id ? -1 : 0);
+
+ return Promise.resolve(flags);
+ } catch ({ message }) {
+ return Promise.reject(message);
+ }
}
- function fetch_bug_summaries(flags) {
- return new Y.Promise(function (resolve, reject) {
- var bug_ids = Y.Array.dedupe(flags.map(function (f) {
- return f.bug_id;
- }));
-
- bugDS.sendRequest({
- request: JSON.stringify({
- version: '1.1',
- method: 'Bug.get',
- params: {
- ids: bug_ids,
- include_fields: ['summary', 'id'],
- Bugzilla_api_token: (BUGZILLA.api_token ? BUGZILLA.api_token : '')
- }
- }),
- cfg: {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' }
- },
- callback: {
- success: function (e) {
- var bugs = e.response.results,
- summary = {};
-
- bugs.forEach(function (bug) {
- summary[bug.id] = bug.summary;
- });
- flags.forEach(function (flag) {
- flag.bug_summary = summary[flag.bug_id];
- });
- resolve(flags);
- },
- failure: function (e) {
- reject(e.error.message);
- }
- }
- });
- });
+ const fetch_bug_summaries = async flags => {
+ const bug_ids = [...(new Set(flags.map(flag => flag.bug_id)))]; // remove duplicates with `Set`
+ const summary = {};
+
+ try {
+ const { bugs } = await Bugzilla.API.get('bug', { id: bug_ids, include_fields: ['id', 'summary'] });
+
+ bugs.forEach(bug => summary[bug.id] = bug.summary);
+ flags.forEach(flag => flag.bug_summary = summary[flag.bug_id]);
+
+ return Promise.resolve(flags);
+ } catch ({ message }) {
+ return Promise.reject(message);
+ }
}
- function fetch_attachment_descriptions(flags) {
- return new Y.Promise(function (resolve, reject) {
- var attachment_ids = Y.Array.dedupe(flags.map(function (f) {
- return f.attachment_id;
- }));
-
- attachmentDS.sendRequest({
- request: JSON.stringify({
- version: '1.1',
- method: 'Bug.attachments',
- params: {
- attachment_ids: attachment_ids,
- include_fields: ['id', 'description'],
- Bugzilla_api_token : (BUGZILLA.api_token ? BUGZILLA.api_token : '')
- }
- }),
- cfg: {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' }
- },
- callback: {
- success: function (e) {
- var attachments = e.response.meta.attachments;
- flags.forEach(function (flag) {
- flag.attachment = attachments[flag.attachment_id];
- });
- resolve(flags);
- },
- failure: function (e) {
- reject(e.error.message);
- }
- }
+ const fetch_attachment_descriptions = async flags => {
+ const attachment_ids = [...(new Set(flags.map(flag => flag.attachment_id)))]; // remove duplicates
+
+ try {
+ const { attachments } = await Bugzilla.API.get(`bug/attachment/${attachment_ids[0]}`, {
+ attachment_ids,
+ include_fields: ['id', 'description'],
});
- });
+
+ flags.forEach(flag => flag.attachment = attachments[flag.attachment_id]);
+
+ return Promise.resolve(flags);
+ } catch ({ message }) {
+ return Promise.reject(message);
+ }
}
function add_historical_action(history, flag, stash, action) {
});
historyTable.set('data', null);
historyTable.showMessage('Loading...');
- fetch_flag_ids(user)
- .then(fetch_flags)
+ fetch_flags(user)
.then(fetch_bug_summaries)
.then(fetch_attachment_descriptions)
.then(function (flags) {
- return new Y.Promise(function (resolve, reject) {
+ return new Promise((resolve, reject) => {
try {
resolve(generate_history(flags, user));
}
}, '0.0.1', {
requires: [
- "node", "datatype-date", "datatable", "datatable-sort", "datatable-message",
- "datatable-datasource", "datasource-io", "datasource-jsonschema", "cookie",
- "gallery-datatable-row-expansion-bmo", "handlebars", "promise"
+ 'node', 'datatype-date', 'datatable', 'datatable-sort', 'datatable-message', 'cookie',
+ 'gallery-datatable-row-expansion-bmo', 'handlebars'
]
});
});
// mfa
$('#mfa-select-totp')
- .click(function(event) {
+ .click(async event => {
event.preventDefault();
$('#mfa').val('TOTP');
$('#mfa-totp-throbber').show();
$('#mfa-totp-issued').hide();
- var url = `${BUGZILLA.config.basepath}rest/user/mfa/totp/enroll` +
- `?Bugzilla_api_token=${encodeURIComponent(BUGZILLA.api_token)}`;
- $.ajax({
- "url": url,
- "contentType": "application/json",
- "processData": false
- })
- .done(function(data) {
+ try {
+ const { png, secret32 } = await Bugzilla.API.get('user/mfa/totp/enroll');
+
$('#mfa-totp-throbber').hide();
var iframe = $('#mfa-enable-totp-frame').contents();
- iframe.find('#qr').attr('src', 'data:image/png;base64,' + data.png);
- iframe.find('#secret').text(data.secret32);
+ iframe.find('#qr').attr('src', `data:image/png;base64,${png}`);
+ iframe.find('#secret').text(secret32);
$('#mfa-totp-issued').show();
$('#mfa-password').focus();
$('#update').attr('disabled', false);
- })
- .fail(function(data) {
+ } catch (ex) {
$('#mfa-totp-throbber').hide();
- if (data.statusText === 'abort')
- return;
- var message = data.responseJSON ? data.responseJSON.message : 'Unexpected Error';
- console.log(message);
- });
+ }
});
$('#mfa-select-duo')
already. */
YAHOO.bugzilla.dupTable = {
- counter: 0,
- dataSource: null,
- updateTable: function(dataTable, product_name, summary_field) {
+ updateTable: async (dataTable, product_name, summary_field) => {
if (summary_field.value.length < 4) return;
- YAHOO.bugzilla.dupTable.counter = YAHOO.bugzilla.dupTable.counter + 1;
- YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
- var json_object = {
- version : "1.1",
- method : "Bug.possible_duplicates",
- id : YAHOO.bugzilla.dupTable.counter,
- params : {
- Bugzilla_api_token: BUGZILLA.api_token,
- product : product_name,
- summary : summary_field.value,
- limit : 7,
- include_fields : [ "id", "summary", "status", "resolution",
- "update_token" ]
- }
- };
- var post_data = JSON.stringify(json_object);
-
- var callback = {
- success: dataTable.onDataReturnInitializeTable,
- failure: dataTable.onDataReturnInitializeTable,
- scope: dataTable,
- argument: dataTable.getState()
- };
dataTable.showTableMessage(dataTable.get("MSG_LOADING"),
YAHOO.widget.DataTable.CLASS_LOADING);
YAHOO.util.Dom.removeClass('possible_duplicates_container',
'bz_default_hidden');
- dataTable.getDataSource().sendRequest(post_data, callback);
+
+ let data = {};
+
+ try {
+ const { bugs } = await Bugzilla.API.get('bug/possible_duplicates', {
+ product: product_name,
+ summary: summary_field.value,
+ limit: 7,
+ include_fields: ['id', 'summary', 'status', 'resolution', 'update_token'],
+ });
+
+ data = { results: bugs };
+ } catch (ex) {
+ data = { error: true };
+ }
+
+ dataTable.onDataReturnInitializeTable('', data);
},
// This is the keyup event handler. It calls updateTable with a relatively
// long delay, to allow additional input. However, the delay is short
el.appendChild(button);
new YAHOO.widget.Button(button);
},
- init_ds: function() {
- var new_ds = new YAHOO.util.XHRDataSource(`${BUGZILLA.config.basepath}jsonrpc.cgi`);
- new_ds.connTimeout = 30000;
- new_ds.connMethodPost = true;
- new_ds.connXhrMode = "cancelStaleRequests";
- new_ds.maxCacheEntries = 3;
- new_ds.responseSchema = {
- resultsList : "result.bugs",
- metaFields : { error: "error", jsonRpcId: "id" }
- };
- // DataSource can't understand a JSON-RPC error response, so
- // we have to modify the result data if we get one.
- new_ds.doBeforeParseData =
- function(oRequest, oFullResponse, oCallback) {
- if (oFullResponse.error) {
- oFullResponse.result = {};
- oFullResponse.result.bugs = [];
- if (console) {
- console.log("JSON-RPC error:", oFullResponse.error);
- }
- }
- return oFullResponse;
- }
-
- this.dataSource = new_ds;
- },
init: function(data) {
- if (this.dataSource == null) this.init_ds();
data.options.initialLoad = false;
- var dt = new YAHOO.widget.DataTable(data.container, data.columns,
- this.dataSource, data.options);
+
+ const ds = new YAHOO.util.LocalDataSource([]); // Dummy data source
+ const dt = new YAHOO.widget.DataTable(data.container, data.columns, ds, data.options);
+
YAHOO.util.Event.on(data.summary_field, 'input', this.doUpdateTable,
[dt, data.product_name]);
}
};
-
-(function(){
- 'use strict';
-
- YAHOO.bugzilla.bugUserLastVisit = {
- update: function(bug_ids) {
- var args = JSON.stringify({
- version: "1.1",
- method: 'BugUserLastVisit.update',
- params: {
- Bugzilla_api_token: BUGZILLA.api_token,
- ids: bug_ids
- }
- });
- var callbacks = {
- failure: function(res) {
- if (console)
- console.log("failed to update last visited: "
- + res.responseText);
- },
- };
-
- YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
- YAHOO.util.Connect.asyncRequest('POST', `${BUGZILLA.config.basepath}jsonrpc.cgi`, callbacks,
- args)
- },
-
- get: function(done) {
- var args = JSON.stringify({
- version: "1.1",
- method: 'BugUserLastVisit.get',
- params: {
- Bugzilla_api_token: BUGZILLA.api_token
- }
- });
- var callbacks = {
- success: function(res) { done(JSON.parse(res.responseText)) },
- failure: function(res) {
- if (console)
- console.log("failed to get last visited: "
- + res.responseText);
- },
- };
- YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
- YAHOO.util.Connect.asyncRequest('POST', `${BUGZILLA.config.basepath}jsonrpc.cgi`, callbacks, args)
- },
- };
-
- YAHOO.bugzilla.bugInterest = {
- unmark: function(bug_ids) {
- var args = JSON.stringify({
- version: "1.1",
- method: 'MyDashboard.bug_interest_unmark',
- params: {
- bug_ids: bug_ids,
- Bugzilla_api_token: (BUGZILLA.api_token ? BUGZILLA.api_token : '')
- },
- });
- var callbacks = {
- failure: function(res) {
- if (console)
- console.log("failed to unmark interest: "
- + res.responseText);
- },
- };
- YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
- YAHOO.util.Connect.asyncRequest('POST', `${BUGZILLA.config.basepath}jsonrpc.cgi`, callbacks, args)
- },
- };
-})();
$('#bz_ctag_add').devbridgeAutocomplete({
appendTo: $('#main-inner'),
forceFixPosition: true,
- serviceUrl: function(query) {
- return `${BUGZILLA.config.basepath}rest/bug/comment/tags/${encodeURIComponent(query)}`;
- },
- params: {
- Bugzilla_api_token: BUGZILLA.api_token
- },
deferRequestBy: 250,
minChars: 1,
tabDisabled: true,
- transformResult: function(response) {
- response = $.parseJSON(response);
- return {
- suggestions: $.map(response, function(dataItem) {
- return {
- value: dataItem,
- data : null
- };
- })
- };
+ lookup: (query, done) => {
+ // Note: `async` doesn't work for this `lookup` function, so use a `Promise` chain instead
+ Bugzilla.API.get(`bug/comment/tags/${encodeURIComponent(query)}`)
+ .then(data => data.map(tag => ({ value: tag })))
+ .catch(() => [])
+ .then(suggestions => done({ suggestions }));
}
});
},
} else {
// show or move
- this.rpcRefresh(comment_id, comment_no);
+ this.fetchRefresh(comment_id, comment_no);
this.current_id = comment_id;
this.current_no = comment_no;
this.ctag_add.value = '';
tags.sort();
// update
this.showTags(comment_id, comment_no, tags);
- this.rpcUpdate(comment_id, comment_no, new_tags, undefined);
+ this.fetchUpdate(comment_id, comment_no, new_tags, undefined);
},
remove : function(comment_id, comment_no, tag) {
var el = Dom.get('ct_' + comment_no + '_' + tag);
if (el) {
el.parentNode.removeChild(el);
- this.rpcUpdate(comment_id, comment_no, undefined, [ tag ]);
+ this.fetchUpdate(comment_id, comment_no, undefined, [ tag ]);
this.hideEmpty(comment_no);
}
},
&& this.pending['c' + comment_id] > 0;
},
- rpcRefresh : function(comment_id, comment_no, noRefreshOnError) {
- this.incPending(comment_id);
- YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
- YAHOO.util.Connect.asyncRequest('POST', `${BUGZILLA.config.basepath}jsonrpc.cgi`,
- {
- success: function(res) {
- YAHOO.bugzilla.commentTagging.decPending(comment_id);
- data = JSON.parse(res.responseText);
- if (data.error) {
- YAHOO.bugzilla.commentTagging.handleRpcError(
- comment_id, comment_no, data.error.message, noRefreshOnError);
- return;
- }
+ fetchRefresh : async (comment_id, comment_no, noRefreshOnError) => {
+ const self = YAHOO.bugzilla.commentTagging;
- if (!YAHOO.bugzilla.commentTagging.hasPending(comment_id))
- YAHOO.bugzilla.commentTagging.showTags(
- comment_id, comment_no, data.result.comments[comment_id].tags);
- },
- failure: function(res) {
- YAHOO.bugzilla.commentTagging.decPending(comment_id);
- YAHOO.bugzilla.commentTagging.handleRpcError(
- comment_id, comment_no, res.responseText, noRefreshOnError);
- }
- },
- JSON.stringify({
- version: "1.1",
- method: 'Bug.comments',
- params: {
- Bugzilla_api_token: BUGZILLA.api_token,
- comment_ids: [ comment_id ],
- include_fields: [ 'tags' ]
+ self.incPending(comment_id);
+
+ try {
+ const { comments } = await Bugzilla.API.get(`bug/comment/${comment_id}?include_fields=tags`);
+
+ self.decPending(comment_id);
+
+ if (!self.hasPending(comment_id)) {
+ self.showTags(comment_id, comment_no, comments[comment_id].tags);
}
- })
- );
+ } catch ({ message }) {
+ self.decPending(comment_id);
+ self.handleFetchError(comment_id, comment_no, message, noRefreshOnError);
+ }
},
- rpcUpdate : function(comment_id, comment_no, add, remove) {
- this.incPending(comment_id);
- YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
- YAHOO.util.Connect.asyncRequest('POST', `${BUGZILLA.config.basepath}jsonrpc.cgi`,
- {
- success: function(res) {
- YAHOO.bugzilla.commentTagging.decPending(comment_id);
- data = JSON.parse(res.responseText);
- if (data.error) {
- YAHOO.bugzilla.commentTagging.handleRpcError(comment_id, comment_no, data.error.message);
- return;
- }
+ fetchUpdate : async (comment_id, comment_no, add, remove) => {
+ const self = YAHOO.bugzilla.commentTagging;
- if (!YAHOO.bugzilla.commentTagging.hasPending(comment_id))
- YAHOO.bugzilla.commentTagging.showTags(comment_id, comment_no, data.result);
- },
- failure: function(res) {
- YAHOO.bugzilla.commentTagging.decPending(comment_id);
- YAHOO.bugzilla.commentTagging.handleRpcError(comment_id, comment_no, res.responseText);
- }
- },
- JSON.stringify({
- version: "1.1",
- method: 'Bug.update_comment_tags',
- params: {
- Bugzilla_api_token: BUGZILLA.api_token,
- comment_id: comment_id,
- add: add,
- remove: remove
+ self.incPending(comment_id);
+
+ try {
+ const data = await Bugzilla.API.put(`bug/comment/${comment_id}/tags`, { comment_id, add, remove });
+
+ if (!self.hasPending(comment_id)) {
+ self.showTags(comment_id, comment_no, data);
}
- })
- );
+ } catch ({ message }) {
+ self.decPending(comment_id);
+ self.handleFetchError(comment_id, comment_no, message);
+ }
},
- handleRpcError : function(comment_id, comment_no, message, noRefreshOnError) {
- YAHOO.bugzilla.commentTagging.showError(comment_id, comment_no, message);
+ handleFetchError : (comment_id, comment_no, message, noRefreshOnError) => {
+ const self = YAHOO.bugzilla.commentTagging;
+
+ self.showError(comment_id, comment_no, message);
+
if (!noRefreshOnError) {
- YAHOO.bugzilla.commentTagging.rpcRefresh(comment_id, comment_no, true);
+ self.fetchRefresh(comment_id, comment_no, true);
}
}
}
var options_user = {
appendTo: $('#main-inner'),
forceFixPosition: true,
- serviceUrl: `${BUGZILLA.config.basepath}rest/user/suggest`,
- params: {
- Bugzilla_api_token: BUGZILLA.api_token,
- },
paramName: 'match',
deferRequestBy: 250,
minChars: 2,
autoSelectFirst: true,
preserveInput: true,
triggerSelectOnValidInput: false,
- transformResult: function(response) {
- response = $.parseJSON(response);
- return {
- suggestions: $.map(response.users, function({ name, real_name, requests, gravatar } = {}) {
- return {
- value: name,
- data : { email: name, real_name, requests, gravatar }
- };
- })
- };
+ lookup: (query, done) => {
+ // Note: `async` doesn't work for this `lookup` function, so use a `Promise` chain instead
+ Bugzilla.API.get('user/suggest', { match: query })
+ .then(({ users }) => users.map(({ name, real_name, requests, gravatar }) => ({
+ value: name,
+ data: { email: name, real_name, requests, gravatar },
+ })))
+ .catch(() => [])
+ .then(suggestions => done({ suggestions }));
},
formatResult: function(suggestion) {
const $input = this;
var last_comment_text = '';
-function show_comment_preview(bug_id) {
+async function show_comment_preview(bug_id) {
var Dom = YAHOO.util.Dom;
var comment = document.getElementById('comment');
var preview = document.getElementById('comment_preview');
Dom.addClass('comment_preview_text', 'bz_default_hidden');
Dom.removeClass('comment_preview_loading', 'bz_default_hidden');
- YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
- YAHOO.util.Connect.asyncRequest('POST', `${BUGZILLA.config.basepath}jsonrpc.cgi`,
- {
- success: function(res) {
- data = JSON.parse(res.responseText);
- if (data.error) {
- Dom.addClass('comment_preview_loading', 'bz_default_hidden');
- Dom.removeClass('comment_preview_error', 'bz_default_hidden');
- Dom.get('comment_preview_error').innerHTML =
- data.error.message.htmlEncode();
- } else {
- $comment_body.innerHTML = data.result.html;
+ try {
+ const { html } = await Bugzilla.API.post('bug/comment/render', { id: bug_id, text: comment.value });
- // Highlight code if possible
- if (Prism) {
- Prism.highlightAllUnder($comment_body);
- }
+ $comment_body.innerHTML = html;
- Dom.addClass('comment_preview_loading', 'bz_default_hidden');
- Dom.removeClass('comment_preview_text', 'bz_default_hidden');
- last_comment_text = comment.value;
- }
- },
- failure: function(res) {
- Dom.addClass('comment_preview_loading', 'bz_default_hidden');
- Dom.removeClass('comment_preview_error', 'bz_default_hidden');
- Dom.get('comment_preview_error').innerHTML =
- res.responseText.htmlEncode();
- }
- },
- JSON.stringify({
- version: "1.1",
- method: 'Bug.render_comment',
- params: {
- Bugzilla_api_token: BUGZILLA.api_token,
- id: bug_id,
- text: comment.value
+ // Highlight code if possible
+ if (Prism) {
+ Prism.highlightAllUnder($comment_body);
}
- })
- );
+
+ Dom.addClass('comment_preview_loading', 'bz_default_hidden');
+ Dom.removeClass('comment_preview_text', 'bz_default_hidden');
+ last_comment_text = comment.value;
+ } catch ({ message }) {
+ Dom.addClass('comment_preview_loading', 'bz_default_hidden');
+ Dom.removeClass('comment_preview_error', 'bz_default_hidden');
+ Dom.get('comment_preview_error').innerHTML = YAHOO.lang.escapeHTML(message);
+ }
}
function show_comment_edit() {
return value;
}
-// ajax wrapper, to simplify error handling and auth
-// TODO: Rewrite this method using Promise (Bug 1380437)
-function bugzilla_ajax(request, done_fn, error_fn) {
- $('#xhr-error').hide('');
- $('#xhr-error').html('');
- if (BUGZILLA.api_token) {
- request.url += (request.url.match('\\?') ? '&' : '?') +
- 'Bugzilla_api_token=' + encodeURIComponent(BUGZILLA.api_token);
- }
- if (request.type != 'GET') {
- request.contentType = 'application/json';
- request.processData = false;
- if (request.data && request.data.constructor === Object) {
- request.data = JSON.stringify(request.data);
- }
- }
- return $.ajax(request)
- .done(function(data) {
- if (data.error) {
- if (!request.hideError) {
- $('#xhr-error').html(data.message);
- $('#xhr-error').show('fast');
- }
- if (error_fn)
- error_fn(data.message);
- }
- else if (done_fn) {
- done_fn(data);
- }
- })
- .fail(function(data) {
- if (data.statusText === 'abort')
- return;
- var message = data.responseJSON ? data.responseJSON.message : 'Unexpected Error'; // all errors are unexpected :)
- if (!request.hideError) {
- $('#xhr-error').html(message);
- $('#xhr-error').show('fast');
- }
- if (error_fn)
- error_fn(message);
- });
-}
-
// html encoding
if (!String.prototype.htmlEncode) {
(function() {
});
YAHOO.bugzilla.instantSearch = {
- counter: 0,
dataTable: null,
dataTableColumns: null,
elContent: null,
currentSearchProduct: '',
onInit: function() {
- YAHOO.util.Connect.setDefaultPostHeader('application/json; charset=UTF-8');
-
this.elContent = Dom.get('content');
this.elList = Dom.get('results');
},
initDataTable: function() {
- var dataSource = new YAHOO.util.XHRDataSource(`${BUGZILLA.config.basepath}jsonrpc.cgi`);
- dataSource.connTimeout = 15000;
- dataSource.connMethodPost = true;
- dataSource.connXhrMode = "cancelStaleRequests";
- dataSource.maxCacheEntries = 3;
- dataSource.responseSchema = {
- resultsList : "result.bugs",
- metaFields : { error: "error", jsonRpcId: "id" }
- };
- // DataSource can't understand a JSON-RPC error response, so
- // we have to modify the result data if we get one.
- dataSource.doBeforeParseData =
- function(oRequest, oFullResponse, oCallback) {
- if (oFullResponse.error) {
- oFullResponse.result = {};
- oFullResponse.result.bugs = [];
- if (console)
- console.error("JSON-RPC error:", oFullResponse.error);
- }
- return oFullResponse;
- };
- dataSource.subscribe('dataErrorEvent',
- function() {
- YAHOO.bugzilla.instantSearch.currentSearchQuery = '';
- }
- );
-
this.dataTable = new YAHOO.widget.DataTable(
'results',
this.dataTableColumns,
- dataSource,
+ new YAHOO.util.LocalDataSource([]), // Dummy data source
{
initialLoad: false,
MSG_EMPTY: 'No matching bugs found.',
YAHOO.bugzilla.instantSearch.doSearch(YAHOO.bugzilla.instantSearch.getContent());
},
- doSearch: function(query) {
+ doSearch: async query => {
if (query.length < 4)
return;
' width="16" height="11">',
YAHOO.widget.DataTable.CLASS_LOADING
);
- var jsonObject = {
- version: "1.1",
- method: "Bug.possible_duplicates",
- id: ++YAHOO.bugzilla.instantSearch.counter,
- params: {
+
+ let data;
+
+ try {
+ const { bugs } = await Bugzilla.API.get('bug/possible_duplicates', {
product: YAHOO.bugzilla.instantSearch.getProduct(),
summary: query,
limit: 20,
- include_fields: [ "id", "summary", "status", "resolution", "component" ]
- }
- };
- if (BUGZILLA.api_token) {
- jsonObject.params.Bugzilla_api_token = BUGZILLA.api_token;
- }
+ include_fields: ['id', 'summary', 'status', 'resolution', 'component'],
+ });
- YAHOO.bugzilla.instantSearch.dataTable.getDataSource().sendRequest(
- JSON.stringify(jsonObject),
- {
- success: YAHOO.bugzilla.instantSearch.onSearchResults,
- failure: YAHOO.bugzilla.instantSearch.onSearchResults,
- scope: YAHOO.bugzilla.instantSearch.dataTable,
- argument: YAHOO.bugzilla.instantSearch.dataTable.getState()
- }
- );
+ data = { results: bugs };
+ } catch (ex) {
+ YAHOO.bugzilla.instantSearch.currentSearchQuery = '';
+ data = { error: true };
+ }
+ YAHOO.bugzilla.instantSearch.dataTable.onDataReturnInitializeTable('', data);
} catch(err) {
if (console)
console.error(err.message);
}
},
- onSearchResults: function(sRequest, oResponse, oPayload) {
- YAHOO.bugzilla.instantSearch.dataTable.onDataReturnInitializeTable(sRequest, oResponse, oPayload);
- },
-
getContent: function() {
var content = this.elContent.value.trim();
// work around chrome bug
if (mo < 18) return 'Last year';
return yy + ' years ago';
}
+
+/**
+ * Reference or define the Bugzilla app namespace.
+ * @namespace
+ */
+var Bugzilla = Bugzilla || {}; // eslint-disable-line no-var
+
+/**
+ * Enable easier access to the Bugzilla REST API.
+ * @hideconstructor
+ * @see https://bugzilla.readthedocs.io/en/latest/api/
+ */
+Bugzilla.API = class API {
+ /**
+ * Initialize the request settings for `fetch()` or `XMLHttpRequest`.
+ * @private
+ * @param {String} endpoint See the {@link Bugzilla.API._fetch} method.
+ * @param {String} [method='GET'] See the {@link Bugzilla.API._fetch} method.
+ * @param {Object} [params] See the {@link Bugzilla.API._fetch} method.
+ * @returns {Request} Request settings including the complete URL, HTTP method, headers and body.
+ */
+ static _init(endpoint, method = 'GET', params = {}) {
+ const url = new URL(`${BUGZILLA.config.basepath}rest/${endpoint}`, location.origin);
+
+ if (method === 'GET') {
+ for (const [key, value] of Object.entries(params)) {
+ if (Array.isArray(value)) {
+ if (['include_fields', 'exclude_fields'].includes(key)) {
+ url.searchParams.set(key, value.join(','));
+ } else {
+ // Because the REST API v1 doesn't support comma-separated values for certain params, array values have to
+ // be appended as duplicated params, so the query string will look like `attachment_ids=1&attachment_ids=2`
+ // instead of `attachment_ids=1,2`.
+ value.forEach(val => url.searchParams.append(key, val));
+ }
+ } else {
+ url.searchParams.set(key, value);
+ }
+ }
+ }
+
+ /** @todo Remove this once Bug 1477163 is solved */
+ url.searchParams.set('Bugzilla_api_token', BUGZILLA.api_token);
+
+ return new Request(url, {
+ method,
+ body: method !== 'GET' ? JSON.stringify(params) : null,
+ credentials: 'same-origin',
+ cache: 'no-cache',
+ });
+ }
+
+ /**
+ * Send a `fetch()` request to a Bugzilla REST API endpoint and return results.
+ * @private
+ * @param {String} endpoint URL path excluding the `/rest/` prefix; may also contain query parameters.
+ * @param {Object} [options] Request options.
+ * @param {String} [options.method='GET'] HTTP request method. POST, GET, PUT, etc.
+ * @param {Object} [options.params] Request parameters. For a GET request, it will be sent as the URL query params.
+ * The values will be automatically URL-encoded, so don't use `encodeURIComponent()` for each. For a non-GET request,
+ * this will be part of the request body. The params can be included in `endpoint` if those are simple (no encoding
+ * required) or if the request is not GET but URL query params are required.
+ * @param {Object} [options.init] Extra options for the `fetch()` method.
+ * @returns {Promise.<(Object|Array.<Object>|Error)>} Response data for a valid response, or an `Error` object for an
+ * error returned from the REST API as well as any other exception including an aborted or failed HTTP request.
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
+ */
+ static async _fetch(endpoint, { method = 'GET', params = {}, init = {} } = {}) {
+ const request = this._init(endpoint, method, params);
+
+ if (!navigator.onLine) {
+ return Promise.reject(new Bugzilla.Error({ name: 'OfflineError', message: 'You are currently offline.' }));
+ }
+
+ return new Promise((resolve, reject) => {
+ const timer = window.setTimeout(() => {
+ reject(new Bugzilla.Error({ name: 'TimeoutError', message: 'Request Timeout' }));
+ }, 30000);
+
+ /** @throws {AbortError} */
+ fetch(request, init).then(response => {
+ /** @throws {SyntaxError} */
+ return response.ok ? response.json() : { error: true };
+ }).then(result => {
+ const { error, code, message } = result;
+
+ if (error) {
+ reject(new Bugzilla.Error({ name: 'APIError', code, message }));
+ } else {
+ resolve(result);
+ }
+ }).catch(({ name, code, message }) => {
+ reject(new Bugzilla.Error({ name, code, detail: message }));
+ });
+
+ window.clearTimeout(timer);
+ });
+ }
+
+ /**
+ * Shorthand for a GET request with the {@link Bugzilla.API._fetch} method.
+ * @param {String} endpoint See the {@link Bugzilla.API._fetch} method.
+ * @param {Object} [params] See the {@link Bugzilla.API._fetch} method.
+ * @param {Object} [init] See the {@link Bugzilla.API._fetch} method.
+ * @returns {Promise.<(Object|Array.<Object>|Error)>} See the {@link Bugzilla.API._fetch} method.
+ */
+ static async get(endpoint, params = {}, init = {}) {
+ return this._fetch(endpoint, { method: 'GET', params, init });
+ }
+
+ /**
+ * Shorthand for a POST request with the {@link Bugzilla.API._fetch} method.
+ * @param {String} endpoint See the {@link Bugzilla.API._fetch} method.
+ * @param {Object} [params] See the {@link Bugzilla.API._fetch} method.
+ * @param {Object} [init] See the {@link Bugzilla.API._fetch} method.
+ * @returns {Promise.<(Object|Array.<Object>|Error)>} See the {@link Bugzilla.API._fetch} method.
+ */
+ static async post(endpoint, params = {}, init = {}) {
+ return this._fetch(endpoint, { method: 'POST', params, init });
+ }
+
+ /**
+ * Shorthand for a PUT request with the {@link Bugzilla.API._fetch} method.
+ * @param {String} endpoint See the {@link Bugzilla.API._fetch} method.
+ * @param {Object} [params] See the {@link Bugzilla.API._fetch} method.
+ * @param {Object} [init] See the {@link Bugzilla.API._fetch} method.
+ * @returns {Promise.<(Object|Array.<Object>|Error)>} See the {@link Bugzilla.API._fetch} method.
+ */
+ static async put(endpoint, params = {}, init = {}) {
+ return this._fetch(endpoint, { method: 'PUT', params, init });
+ }
+
+ /**
+ * Shorthand for a PATCH request with the {@link Bugzilla.API._fetch} method.
+ * @param {String} endpoint See the {@link Bugzilla.API._fetch} method.
+ * @param {Object} [params] See the {@link Bugzilla.API._fetch} method.
+ * @param {Object} [init] See the {@link Bugzilla.API._fetch} method.
+ * @returns {Promise.<(Object|Array.<Object>|Error)>} See the {@link Bugzilla.API._fetch} method.
+ */
+ static async patch(endpoint, params = {}, init = {}) {
+ return this._fetch(endpoint, { method: 'PATCH', params, init });
+ }
+
+ /**
+ * Shorthand for a DELETE request with the {@link Bugzilla.API._fetch} method.
+ * @param {String} endpoint See the {@link Bugzilla.API._fetch} method.
+ * @param {Object} [params] See the {@link Bugzilla.API._fetch} method.
+ * @param {Object} [init] See the {@link Bugzilla.API._fetch} method.
+ * @returns {Promise.<(Object|Array.<Object>|Error)>} See the {@link Bugzilla.API._fetch} method.
+ */
+ static async delete(endpoint, params = {}, init = {}) {
+ return this._fetch(endpoint, { method: 'DELETE', params, init });
+ }
+
+ /**
+ * Success callback function for the {@link Bugzilla.API.xhr} method.
+ * @callback Bugzilla.API~resolve
+ * @param {Object|Array.<Object>} data Response data for a valid response.
+ */
+
+ /**
+ * Error callback function for the {@link Bugzilla.API.xhr} method.
+ * @callback Bugzilla.API~reject
+ * @param {Error} error `Error` object providing a reason. See the {@link Bugzilla.Error} for details.
+ */
+
+ /**
+ * Make an `XMLHttpRequest` to a Bugzilla REST API endpoint and return results. This is useful when you need more
+ * control than `fetch()`, particularly to upload a file while monitoring the progress.
+ * @param {String} endpoint See the {@link Bugzilla.API._fetch} method.
+ * @param {Object} [options] Request options.
+ * @param {String} [options.method='GET'] See the {@link Bugzilla.API._fetch} method.
+ * @param {Object} [options.params] See the {@link Bugzilla.API._fetch} method.
+ * @param {Bugzilla.API~resolve} [options.resolve] Callback function for a valid response.
+ * @param {Bugzilla.API~reject} [options.reject] Callback function for an error returned from the REST API as well as
+ * any other exception including an aborted or failed HTTP request.
+ * @param {Object.<String, Function>} [options.download] Raw event listeners for download; the key is an event type
+ * such as `load` or `error`, the value is an event listener.
+ * @param {Object.<String, Function>} [options.upload] Raw event listeners for upload; the key is an event type such
+ * as `progress` or `error`, the value is an event listener.
+ * @returns {XMLHttpRequest} Request.
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Monitoring_progress
+ */
+ static xhr(endpoint, { method = 'GET', params = {}, resolve, reject, download, upload } = {}) {
+ const xhr = new XMLHttpRequest();
+ const { url, headers, body } = this._init(endpoint, method, params);
+
+ resolve = typeof resolve === 'function' ? resolve : () => {};
+ reject = typeof reject === 'function' ? reject : () => {};
+
+ if (!navigator.onLine) {
+ reject(new Bugzilla.Error({ name: 'OfflineError', message: 'You are currently offline.' }));
+
+ return xhr;
+ }
+
+ xhr.addEventListener('load', () => {
+ try {
+ /** @throws {SyntaxError} */
+ const result = JSON.parse(xhr.responseText);
+ const { error, code, message } = result;
+
+ if (parseInt(xhr.status / 100) !== 2 || error) {
+ reject(new Bugzilla.Error({ name: 'APIError', code, message }));
+ } else {
+ resolve(result);
+ }
+ } catch ({ name, code, message }) {
+ reject(new Bugzilla.Error({ name, code, detail: message }));
+ }
+ });
+
+ xhr.addEventListener('abort', () => {
+ reject(new Bugzilla.Error({ name: 'AbortError' }));
+ });
+
+ xhr.addEventListener('error', () => {
+ reject(new Bugzilla.Error({ name: 'NetworkError' }));
+ });
+
+ xhr.addEventListener('timeout', () => {
+ reject(new Bugzilla.Error({ name: 'TimeoutError', message: 'Request Timeout' }));
+ });
+
+ for (const [type, handler] of Object.entries(download || {})) {
+ xhr.addEventListener(type, event => handler(event));
+ }
+
+ for (const [type, handler] of Object.entries(upload || {})) {
+ xhr.upload.addEventListener(type, event => handler(event));
+ }
+
+ xhr.open(method, url);
+
+ for (const [key, value] of headers) {
+ xhr.setRequestHeader(key, value);
+ }
+
+ // Set timeout in 30 seconds given some large results
+ xhr.timeout = 30000;
+
+ xhr.send(body);
+
+ return xhr;
+ }
+};
+
+/**
+ * Extend the generic `Error` class so it can contain a custom name and other data if needed. This allows to gracefully
+ * handle an error from either the REST API or the browser.
+ */
+Bugzilla.Error = class CustomError extends Error {
+ /**
+ * Initialize the `CustomError` object.
+ * @param {Object} [options] Error options.
+ * @param {String} [options.name='Error'] Distinguishable error name that can be taken from an original `DOMException`
+ * object if any, e.g. `AbortError` or `SyntaxError`.
+ * @param {String} [options.message='Unexpected Error'] Localizable, user-friendly message probably from the REST API.
+ * @param {Number} [options.code=0] Custom error code from the REST API or `DOMException` code.
+ * @param {String} [options.detail] Detailed, technical message probably from an original `DOMException` that end
+ * users don't have to see.
+ */
+ constructor({ name = 'Error', message = 'Unexpected Error', code = 0, detail } = {}) {
+ super(message);
+ this.name = name;
+ this.code = code;
+ this.detail = detail;
+
+ console.error(this.toString());
+ }
+
+ /**
+ * Define the string representation of the error object.
+ * @override
+ * @returns {String} Custom string representation.
+ */
+ toString() {
+ return `${this.name}: "${this.message}" (code: ${this.code}${this.detail ? `, detail: ${this.detail}` : ''})`;
+ }
+};
initDirtyFieldTracking();
[% IF user.id %]
- YAHOO.bugzilla.bugUserLastVisit.update([ [% bug.bug_id FILTER none %] ]);
+ Bugzilla.API.post('bug_user_last_visit/[% bug.bug_id FILTER none %]');
[% END %]
});
BUGZILLA.bug_id = [% bug.id FILTER none %];
no_yui = 0
jquery = []
jquery_css = []
- generate_api_token = 0
+ generate_api_token = 1
robots = 'index'
%]