Commit 225503e7 authored by Lukas Eipert's avatar Lukas Eipert

Simplify implementation of AjaxLoadingSpinner

The AjaxLoadingSpinner is only used on our branches page. This
simplifies the implementation to not show the button again after the
delete call is finished, as the row is removed anyhow.

We are also replacing the spinner with a proper GlSpinner and the
fontawesome icons with the real sprite_icons.

Furthermore disabled buttons and buttons that actually open a modal for
protected branches had the same event listeners attached. This is not
necessary, as the event listeners were never called.

The files were moved into the branches sub folder as it is only used on
that page.

The test suite has been simplified as well and instead of mocking out
jQuery ajax, we just test the event handler directly. This is work which
helps us with the jquery-ujs to @rails/ujs migration.
parent 63213f5b
import $ from 'jquery';
export default class AjaxLoadingSpinner {
static init() {
const $elements = $('.js-ajax-loading-spinner');
$elements.on('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
$elements.on('ajax:complete', AjaxLoadingSpinner.ajaxComplete);
}
static ajaxBeforeSend(e) {
e.target.setAttribute('disabled', '');
const iconElement = e.target.querySelector('i');
// get first fa- icon
const originalIcon = iconElement.className.match(/(fa-)([^\s]+)/g)[0];
iconElement.dataset.icon = originalIcon;
AjaxLoadingSpinner.toggleLoadingIcon(iconElement);
$(e.target).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
}
static ajaxComplete(e) {
e.target.removeAttribute('disabled');
const iconElement = e.target.querySelector('i');
AjaxLoadingSpinner.toggleLoadingIcon(iconElement);
$(e.target).off('ajax:complete', AjaxLoadingSpinner.ajaxComplete);
}
static toggleLoadingIcon(iconElement) {
const { classList } = iconElement;
classList.toggle(iconElement.dataset.icon);
classList.toggle('gl-spinner');
classList.toggle('gl-spinner-orange');
classList.toggle('gl-spinner-sm');
}
}
import $ from 'jquery';
export default class AjaxLoadingSpinner {
static init() {
const $elements = $('.js-ajax-loading-spinner');
$elements.on('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
}
static ajaxBeforeSend(e) {
const button = e.target;
const newButton = document.createElement('button');
newButton.classList.add('btn', 'btn-default', 'disabled', 'gl-button');
newButton.setAttribute('disabled', 'disabled');
const spinner = document.createElement('span');
spinner.classList.add('align-text-bottom', 'gl-spinner', 'gl-spinner-sm', 'gl-spinner-orange');
newButton.appendChild(spinner);
button.classList.add('hidden');
button.parentNode.insertBefore(newButton, button.nextSibling);
$(button).one('ajax:error', () => {
newButton.remove();
button.classList.remove('hidden');
});
$(button).one('ajax:success', () => {
$(button).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
});
}
}
import AjaxLoadingSpinner from '~/ajax_loading_spinner'; import AjaxLoadingSpinner from '~/branches/ajax_loading_spinner';
import DeleteModal from '~/branches/branches_delete_modal'; import DeleteModal from '~/branches/branches_delete_modal';
import initDiverganceGraph from '~/branches/divergence_graph'; import initDiverganceGraph from '~/branches/divergence_graph';
......
...@@ -50,25 +50,25 @@ ...@@ -50,25 +50,25 @@
- if can?(current_user, :push_code, @project) - if can?(current_user, :push_code, @project)
- if branch.name == @project.repository.root_ref - if branch.name == @project.repository.root_ref
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled", %button{ class: "btn btn-remove remove-row has-tooltip disabled",
disabled: true, disabled: true,
title: s_('Branches|The default branch cannot be deleted') } title: s_('Branches|The default branch cannot be deleted') }
= icon("trash-o") = sprite_icon("remove")
- elsif protected_branch?(@project, branch) - elsif protected_branch?(@project, branch)
- if can?(current_user, :push_to_delete_protected_branch, @project) - if can?(current_user, :push_to_delete_protected_branch, @project)
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip", %button{ class: "btn btn-remove remove-row has-tooltip",
title: s_('Branches|Delete protected branch'), title: s_('Branches|Delete protected branch'),
data: { toggle: "modal", data: { toggle: "modal",
target: "#modal-delete-branch", target: "#modal-delete-branch",
delete_path: project_branch_path(@project, branch.name), delete_path: project_branch_path(@project, branch.name),
branch_name: branch.name, branch_name: branch.name,
is_merged: ("true" if merged) } } is_merged: ("true" if merged) } }
= icon("trash-o") = sprite_icon("remove")
- else - else
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled", %button{ class: "btn btn-remove remove-row has-tooltip disabled",
disabled: true, disabled: true,
title: s_('Branches|Only a project maintainer or owner can delete a protected branch') } title: s_('Branches|Only a project maintainer or owner can delete a protected branch') }
= icon("trash-o") = sprite_icon("remove")
- else - else
= link_to project_branch_path(@project, branch.name), = link_to project_branch_path(@project, branch.name),
class: "btn btn-remove remove-row qa-remove-btn js-ajax-loading-spinner has-tooltip", class: "btn btn-remove remove-row qa-remove-btn js-ajax-loading-spinner has-tooltip",
...@@ -77,4 +77,4 @@ ...@@ -77,4 +77,4 @@
data: { confirm: s_("Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?") % { branch_name: branch.name } }, data: { confirm: s_("Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?") % { branch_name: branch.name } },
remote: true, remote: true,
'aria-label' => s_('Branches|Delete branch') do 'aria-label' => s_('Branches|Delete branch') do
= icon("trash-o") = sprite_icon("remove")
---
title: Change icon for branch delete button
merge_request: 39968
author:
type: changed
import $ from 'jquery';
import AjaxLoadingSpinner from '~/ajax_loading_spinner';
describe('Ajax Loading Spinner', () => {
const fixtureTemplate = 'static/ajax_loading_spinner.html';
preloadFixtures(fixtureTemplate);
beforeEach(() => {
loadFixtures(fixtureTemplate);
AjaxLoadingSpinner.init();
});
it('change current icon with spinner icon and disable link while waiting ajax response', done => {
jest.spyOn($, 'ajax').mockImplementation(req => {
const xhr = new XMLHttpRequest();
const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner');
const icon = ajaxLoadingSpinner.querySelector('i');
req.beforeSend(xhr, { dataType: 'text/html' });
expect(icon).not.toHaveClass('fa-trash-o');
expect(icon).toHaveClass('gl-spinner');
expect(icon).toHaveClass('gl-spinner-orange');
expect(icon).toHaveClass('gl-spinner-sm');
expect(icon.dataset.icon).toEqual('fa-trash-o');
expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual('');
req.complete({});
done();
const deferred = $.Deferred();
return deferred.promise();
});
document.querySelector('.js-ajax-loading-spinner').click();
});
it('use original icon again and enabled the link after complete the ajax request', done => {
jest.spyOn($, 'ajax').mockImplementation(req => {
const xhr = new XMLHttpRequest();
const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner');
req.beforeSend(xhr, { dataType: 'text/html' });
req.complete({});
const icon = ajaxLoadingSpinner.querySelector('i');
expect(icon).toHaveClass('fa-trash-o');
expect(icon).not.toHaveClass('gl-spinner');
expect(icon).not.toHaveClass('gl-spinner-orange');
expect(icon).not.toHaveClass('gl-spinner-sm');
expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual(null);
done();
const deferred = $.Deferred();
return deferred.promise();
});
document.querySelector('.js-ajax-loading-spinner').click();
});
});
import AjaxLoadingSpinner from '~/branches/ajax_loading_spinner';
describe('Ajax Loading Spinner', () => {
let ajaxLoadingSpinnerElement;
let fauxEvent;
beforeEach(() => {
document.body.innerHTML = `
<div>
<a class="js-ajax-loading-spinner"
data-remote
href="http://goesnowhere.nothing/whereami">
<i class="fa fa-trash-o"></i>
</a></div>`;
AjaxLoadingSpinner.init();
ajaxLoadingSpinnerElement = document.querySelector('.js-ajax-loading-spinner');
fauxEvent = { target: ajaxLoadingSpinnerElement };
});
afterEach(() => {
document.body.innerHTML = '';
});
it('`ajaxBeforeSend` event handler sets current icon to spinner and disables link', () => {
expect(ajaxLoadingSpinnerElement.parentNode.querySelector('.gl-spinner')).toBeNull();
expect(ajaxLoadingSpinnerElement.classList.contains('hidden')).toBe(false);
AjaxLoadingSpinner.ajaxBeforeSend(fauxEvent);
expect(ajaxLoadingSpinnerElement.parentNode.querySelector('.gl-spinner')).not.toBeNull();
expect(ajaxLoadingSpinnerElement.classList.contains('hidden')).toBe(true);
});
});
<a class="js-ajax-loading-spinner" data-remote href="http://goesnowhere.nothing/whereami">
<i class="fa fa-trash-o"></i>
</a>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment