Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/modal global loading indicator #229

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions src/mw-modal/directives/mw_modal.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
angular.module('mwUI.Modal')

.directive('mwModal', function (mwModalTmpl) {
.directive('mwModal', function (mwModalTmpl, Loading) {
return {
restrict: 'A',
scope: {
title: '@'
},
transclude: true,
templateUrl: 'uikit/mw-modal/directives/templates/mw_modal.html',
controller: function($scope){
this.addClass = function(styleClass){
controller: function ($scope) {
this.addClass = function (styleClass) {
$scope.addClass(styleClass);
};
},
link: function (scope, el) {
scope.$emit('COMPILE:FINISHED');
scope.mwModalTmpl = mwModalTmpl;
scope.addClass = function(styleClass){
scope.addClass = function (styleClass) {
el.addClass(styleClass);
};

if(scope.title){
if (scope.title) {
scope.addClass('has-header');
}

scope.showLoadingSpinner = function () {
return Loading.isLoading();
};
}
};
});
4 changes: 4 additions & 0 deletions src/mw-modal/directives/templates/mw_modal.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
ng-src="{{mwModalTmpl.getLogoPath()}}"
class="pull-left logo"/>
<h4 class="modal-title pull-left">{{ title }}</h4>
<div ng-if="showLoadingSpinner()"
class="loading-spinner">
<div mw-spinner></div>
</div>
</div>
<div class="body-holder">
<div mw-toasts class="notifications"></div>
Expand Down
9 changes: 9 additions & 0 deletions src/mw-modal/styles/_mw_modal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,22 @@ $windowPadding: 60;
box-shadow: 0 -1px 4px rgba(122,122,122,.5);
position: relative;
z-index: 1000;
display: flex;
overflow: hidden;
white-space: nowrap;

.logo{
margin-top: 4px;
margin-right: 5px;
height: 16px;
}

h4{
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
}

}

.body-holder{
Expand Down
22 changes: 21 additions & 1 deletion src/mw-utils/mw_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ angular.module('mwUI.Utils', ['mwUI.i18n','mwUI.Modal']);
// @include ./services/mw_bootstrap_breakpoint.js
// @include ./services/mw_browser_title_handler.js
// @include ./services/mw_callback_handler.js
// @include ./services/mw_loading_service.js
// @include ./services/mw_scheduler.js
// @include ./services/mw_url_storage.js
// @include ./services/mw_runtime_storage.js
Expand All @@ -35,6 +36,25 @@ angular.module('mwUI.Utils', ['mwUI.i18n','mwUI.Modal']);
// @include ./shims/route_to_regex.js
// @include ./shims/deprecation_warning.js

angular.module('mwUI.Utils').config(function(i18nProvider){
angular.module('mwUI.Utils').config(function($provide, $httpProvider, i18nProvider){
i18nProvider.addResource('mw-utils/i18n', 'uikit');

$provide.factory('requestInterceptorForLoadingService', function ($q, Loading) {
return {
request: function(request){
Loading.start();
return request;
},
response: function (response) {
Loading.done();
return response;
},
responseError: function (response) {
Loading.done();
return $q.reject(response);
}
};
});

$httpProvider.interceptors.push('requestInterceptorForLoadingService');
});
142 changes: 142 additions & 0 deletions src/mw-utils/services/mw_loading_service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
'use strict';

angular.module('mwUI.Utils')

/**
* @ngdoc service
* @name mwUI.Utils.service:Loading
*
* @description
* This service manages loading processes. It can be used e.g. by a httpInterceptor to register a loading process
*/
.service('Loading', function ($timeout) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have a service called 'Loading' (see relution/common). How are name collisions handled

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will use the one the is registered first, the other one is just ignored. Next step is to remove the Loading service from the portal

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So in fact you moved the loading service into the UI-Kit...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes with some small modifications, a merge request for the portal will come as soon as this mr is released in a new uikit version


var itemsToLoad = 0,
itemsAlreadyLoaded = 0,
loading = false,
keysToLoad = {},
waitUntilDoneTimout,
doneCallbacks = [],
startCallbacks = [];

var reset = function () {
itemsToLoad = 0;
itemsAlreadyLoaded = 0;
};

var executeCallbacks = function (callbacks, args, scope) {
scope = scope || this;
callbacks.forEach(function (callback) {
callback.apply(scope, args);
});
};

var setToDone = function () {
if (waitUntilDoneTimout) {
$timeout.cancel(waitUntilDoneTimout);
}
waitUntilDoneTimout = $timeout(function () {
loading = false;
executeCallbacks(doneCallbacks);
reset();
}, 100);
};

var registerCallback = function (array, callback) {
if (typeof callback === 'function') {
array.push(callback);
} else {
throw new Error('Callback has to be a function');
}
};

/**
* @ngdoc function
* @name start
* @methodOf mwUI.Utils.service:Loading
* @description
* Starts a loading process. When a key is passed a specified loading process is registered otherwise
* a global loading process is registered
* @param {String} key a unique key to start the loading process
*/
this.start = function (key) {
if (key) {
keysToLoad[key] = true;
} else {
itemsToLoad++;
if (!loading) {
executeCallbacks(startCallbacks);
loading = true;
$timeout.cancel(waitUntilDoneTimout);
} else {
$timeout.cancel(waitUntilDoneTimout);
}
}
};

/**
* @ngdoc function
* @name done
* @methodOf mwUI.Utils.service:Loading
* @description
* Stops a loading process identified by a unique key.
* When no key is specified a global loading process counter is increased. As soon as the global loading
* process counter is the same as the length of registered global loading processes a debounced callback is called
* @param {String} key the unique key which belongs to the animation (optional)
*/
this.done = function (key) {
if (key) {
delete keysToLoad[key];
} else {
if (itemsToLoad !== 0) {
itemsAlreadyLoaded++;
if (itemsToLoad === itemsAlreadyLoaded) {
setToDone();
}
}
}
};

/**
* @ngdoc function
* @name isLoading
* @methodOf mwUI.Utils.service:Loading
* @description
* When a key is specified it returns whether the loading process for that key is active
* otherwise it returns whether at least one process is active or none at all
* @param {String} key the unique key which belongs to the loading process (optional)
* @return {Boolean} Returns true if a loading is currently active for the given key
*/
this.isLoading = function (key) {
if (key) {
return keysToLoad[key] || false;
} else {
return loading;
}
};

/**
* @ngdoc function
* @name registerDoneCallback
* @methodOf mwUI.Utils.service:Loading
* @description
* Registers a callback function which gets called when all loading processes are done
* @param {Function} callback the callback function
*/
this.registerDoneCallback = function (callback) {
registerCallback(doneCallbacks, callback);
};

/**
* @ngdoc function
* @name registerStartCallback
* @methodOf mwUI.Utils.service:Loading
* @description
* Registers a callback function which gets called when the loading starts
* @param {Function} callback the callback function
*/
this.registerStartCallback = function (callback) {
registerCallback(startCallbacks, callback);
};

});
93 changes: 93 additions & 0 deletions src/mw-utils/services/mw_loading_service_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use strict';

describe('Loading', function () {
var service, rootScope;
var $timeout;
beforeEach(module('mwUI.Utils'));

afterEach(function () {
$timeout.verifyNoPendingTasks();
service = null;
rootScope = null;
$timeout = null;
});

beforeEach(inject(function (_$rootScope_, _Loading_, _$timeout_) {
rootScope = _$rootScope_;
service = _Loading_;
$timeout = _$timeout_;
}));

it('can be initialized', function () {
expect(service).toBeDefined();
});

it('stores a loading key', function () {
service.start('test-key');
expect(service.isLoading('test-key')).toBeTruthy();
service.done('test-key');
expect(service.isLoading('test-key')).toBeFalsy();
});

it('isLoading can handle undefined as key', function () {
expect(service.isLoading(undefined)).toBeFalsy();
});

it('isLoading can handle undefined as key and returns true when global loading process is in progress', function () {
service.start();
expect(service.isLoading()).toBeTruthy();
});

it('isLoading can handle undefined as key and returns false when no global loading process is in progress anymore', function () {
service.start();
service.done();
$timeout.flush();

expect(service.isLoading()).toBeFalsy();
});

it('isLoading can handle undefined as key and returns true when at least one global loading process is in progress', function () {
service.start();
service.start();

service.done();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add $timeout.flush()... i expect that the test will crash

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it will fail because there are no $timeouts to flush but this is the expected behaviour

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a check in afterEach to verify that there is no outstanding $timeout task

expect(service.isLoading()).toBeTruthy();
});

it('isLoading can handle undefined as key and returns false when all global loading process are done', function () {
service.start();
service.start();

service.done();
service.done();
$timeout.flush();

expect(service.isLoading()).toBeFalsy();
});

it('isLoading can handle null as key', function () {
expect(service.isLoading(null)).toBeFalsy();
});

it('isLoading can unknown key', function () {
expect(service.isLoading('something')).toBeFalsy();
});

it('sets a loading session with callbacks when no key is provided', function () {
var startCallback = jasmine.createSpy('startSpy');
var doneCallback = jasmine.createSpy('doneSpy');
service.registerStartCallback(startCallback);
service.registerDoneCallback(doneCallback);
service.start();
rootScope.$digest();
expect(service.isLoading()).toBeTruthy();
service.done();
rootScope.$digest();
$timeout.flush();
expect(service.isLoading()).toBeFalsy();

expect(startCallback).toHaveBeenCalled();
expect(doneCallback).toHaveBeenCalled();
});
});