diff --git a/src/ui/autoload.js b/src/ui/autoload.js index 6ac6c25de074..df322c491e54 100644 --- a/src/ui/autoload.js +++ b/src/ui/autoload.js @@ -55,6 +55,7 @@ exports.reload = function () { 'ui/persisted_log', 'ui/private', 'ui/promises', + 'ui/safe_confirm', 'ui/state_management/app_state', 'ui/state_management/global_state', 'ui/storage', diff --git a/src/ui/public/courier/courier.js b/src/ui/public/courier/courier.js index 6725f6dfb1a7..a9caf469089d 100644 --- a/src/ui/public/courier/courier.js +++ b/src/ui/public/courier/courier.js @@ -4,6 +4,7 @@ define(function (require) { require('ui/es'); require('ui/promises'); + require('ui/safe_confirm'); require('ui/index_patterns'); require('ui/modules').get('kibana/courier') diff --git a/src/ui/public/courier/saved_object/saved_object.js b/src/ui/public/courier/saved_object/saved_object.js index 63a61eddd34f..439a805fd45f 100644 --- a/src/ui/public/courier/saved_object/saved_object.js +++ b/src/ui/public/courier/saved_object/saved_object.js @@ -1,5 +1,5 @@ define(function (require) { - return function SavedObjectFactory(es, kbnIndex, Promise, Private, Notifier, indexPatterns) { + return function SavedObjectFactory(es, kbnIndex, Promise, Private, Notifier, safeConfirm, indexPatterns) { var angular = require('angular'); var errors = require('ui/errors'); var _ = require('lodash'); @@ -252,12 +252,12 @@ define(function (require) { if (_.get(err, 'origError.status') === 409) { var confirmMessage = 'Are you sure you want to overwrite ' + self.title + '?'; - if (window.confirm(confirmMessage)) { // eslint-disable-line no-alert - return docSource.doIndex(source).then(finish); - } - - // if the user doesn't overwrite record, just swallow the error - return; + return safeConfirm(confirmMessage).then( + function () { + return docSource.doIndex(source).then(finish); + }, + _.noop // if the user doesn't overwrite record, just swallow the error + ); } return Promise.reject(err); }); diff --git a/src/ui/public/safe_confirm/__tests__/safe_confirm.js b/src/ui/public/safe_confirm/__tests__/safe_confirm.js new file mode 100644 index 000000000000..8952cec32f9b --- /dev/null +++ b/src/ui/public/safe_confirm/__tests__/safe_confirm.js @@ -0,0 +1,82 @@ +describe('ui/safe_confirm', function () { + var sinon = require('sinon'); + var expect = require('expect.js'); + var ngMock = require('ngMock'); + + var $rootScope; + var $window; + var $timeout; + var message; + var safeConfirm; + var promise; + + beforeEach(function () { + ngMock.module('kibana', function ($provide) { + $provide.value('$window', { + confirm: sinon.stub().returns(true) + }); + }); + + ngMock.inject(function ($injector) { + safeConfirm = $injector.get('safeConfirm'); + $rootScope = $injector.get('$rootScope'); + $window = $injector.get('$window'); + $timeout = $injector.get('$timeout'); + }); + + message = 'woah'; + + promise = safeConfirm(message); + }); + + context('before timeout completes', function () { + it('$window.confirm is not invoked', function () { + expect($window.confirm.called).to.be(false); + }); + it('returned promise is not resolved', function () { + var isResolved = false; + function markAsResolved() { + isResolved = true; + } + promise.then(markAsResolved, markAsResolved); + $rootScope.$apply(); // attempt to resolve the promise, but this won't flush $timeout promises + expect(isResolved).to.be(false); + }); + }); + + context('after timeout completes', function () { + it('$window.confirm is invoked with message', function () { + $timeout.flush(); + expect($window.confirm.calledWith(message)).to.be(true); + }); + + context('when confirmed', function () { + it('promise is fulfilled with true', function () { + $timeout.flush(); + + var value; + promise.then(function (v) { + value = v; + }); + $rootScope.$apply(); + + expect(value).to.be(true); + }); + }); + + context('when canceled', function () { + it('promise is rejected with false', function () { + $window.confirm.returns(false); // must be set before $timeout.flush() + $timeout.flush(); + + var value; + promise.then(null, function (v) { + value = v; + }); + $rootScope.$apply(); + + expect(value).to.be(false); + }); + }); + }); +}); diff --git a/src/ui/public/safe_confirm/safe_confirm.js b/src/ui/public/safe_confirm/safe_confirm.js new file mode 100644 index 000000000000..edbd1a6f9f15 --- /dev/null +++ b/src/ui/public/safe_confirm/safe_confirm.js @@ -0,0 +1,31 @@ +define(function (require) { + require('ui/modules').get('kibana') + + /* + * Angular doesn't play well with thread blocking calls such as + * window.confirm() unless those calls are specifically handled inside a call + * to $timeout(). Rather than litter the code with that implementation + * detail, safeConfirm() can be used. + * + * WARNING: safeConfirm differs from a native call to window.confirm in that + * it only blocks the thread beginning on the next tick. For that reason, a + * promise is returned so consumers can handle the control flow. + * + * Usage: + * safeConfirm('This message will be passed to window.confirm()').then( + * function () { + * // user clicked confirm + * }, + * function () { + * // user canceled the confirmation + * } + * ); + */ + .factory('safeConfirm', function ($window, $timeout, $q) { + return function safeConfirm(message) { + return $timeout(function () { + return $window.confirm(message) || $q.reject(false); + }); + }; + }); +});