mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Upgrade Angular to 1.6.5 (#13543)
* [angular/$http] remove use of .success() and .error() callbacks
* [angular/$route] remove `!` hash prefix
* [angular] upgrade
* [angular/$timeout] prevent unhandled exception "canceled" logging
* [ui/fancy_form] refactor FormController for compatibility
* [ngModelController] ensure method calls keep context
* [ui/queryBar/tests] attach $elem to DOM so "click" triggers "submit"
* [confirmModalPromise] fix test for rejected promise
* [watchMulti] specifically check watchers array for length
* [typeahead] check for property rather than own keys
* [ui/compat] add initAfterBindingsWorkaround
* [ui/fancyForms] fix _getInvalidModels()
* [fancyForm] add tests that check nested form error counting
* [ui/fancyForms] ensure that submit is blocked properly
* [ui/fancyForms] escalate soft errors on failed submit
* [ui/fancyForms] bind handlers to this in constructor
* [uiBootstrap/tooltip] describe the new error handling
* [ui/confirmModalPromise] use more sinon assertions
* [$http] resp => data before old .success() and .error() handlers
* [indices/createWizard] apply callAfterBindings workarounds
(cherry picked from commit 2eae80cd0c
)
This commit is contained in:
parent
ba05ad56b0
commit
74da002ee5
26 changed files with 442 additions and 148 deletions
|
@ -78,7 +78,7 @@
|
|||
"@elastic/webpack-directory-name-as-main": "2.0.2",
|
||||
"JSONStream": "1.1.1",
|
||||
"accept-language-parser": "1.2.0",
|
||||
"angular": "1.4.7",
|
||||
"angular": "1.6.5",
|
||||
"angular-bootstrap-colorpicker": "3.0.19",
|
||||
"angular-elastic": "2.5.0",
|
||||
"angular-route": "1.4.7",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import { callAfterBindingsWorkaround } from 'ui/compat';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import contextAppTemplate from './app.html';
|
||||
import './components/loading_button';
|
||||
|
@ -28,7 +29,7 @@ const module = uiModules.get('apps/context', [
|
|||
module.directive('contextApp', function ContextApp() {
|
||||
return {
|
||||
bindToController: true,
|
||||
controller: ContextAppController,
|
||||
controller: callAfterBindingsWorkaround(ContextAppController),
|
||||
controllerAs: 'contextApp',
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import _ from 'lodash';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { callAfterBindingsWorkaround } from 'ui/compat';
|
||||
import contextSizePickerTemplate from './size_picker.html';
|
||||
import './size_picker.less';
|
||||
|
||||
|
@ -10,7 +11,7 @@ const module = uiModules.get('apps/context', [
|
|||
module.directive('contextSizePicker', function ContextSizePicker() {
|
||||
return {
|
||||
bindToController: true,
|
||||
controller: ContextSizePickerController,
|
||||
controller: callAfterBindingsWorkaround(ContextSizePickerController),
|
||||
controllerAs: 'contextSizePicker',
|
||||
link: linkContextSizePicker,
|
||||
replace: true,
|
||||
|
|
|
@ -23,9 +23,13 @@ const MetricsRequestHandlerProvider = function (Private, Notifier, config, timef
|
|||
try {
|
||||
const maxBuckets = config.get('metrics:max_buckets');
|
||||
validateInterval(timefilter, panel, maxBuckets);
|
||||
return $http.post('../api/metrics/vis/data', params)
|
||||
.success(resolve)
|
||||
.error(resp => {
|
||||
const httpResult = $http.post('../api/metrics/vis/data', params)
|
||||
.then(resp => resp.data)
|
||||
.catch(resp => { throw resp.data; });
|
||||
|
||||
return httpResult
|
||||
.then(resolve)
|
||||
.catch(resp => {
|
||||
resolve({});
|
||||
const err = new Error(resp.message);
|
||||
err.stack = resp.stack;
|
||||
|
|
|
@ -6,13 +6,17 @@ const FetchFieldsProvider = (Notifier, $http) => {
|
|||
const fields = {};
|
||||
|
||||
Promise.all(indexPatterns.map(pattern => {
|
||||
return $http.get(`../api/metrics/fields?index=${pattern}`)
|
||||
.success(resp => {
|
||||
const httpResult = $http.get(`../api/metrics/fields?index=${pattern}`)
|
||||
.then(resp => resp.data)
|
||||
.catch(resp => { throw resp.data; });
|
||||
|
||||
return httpResult
|
||||
.then(resp => {
|
||||
if (resp.length && pattern) {
|
||||
fields[pattern] = resp;
|
||||
}
|
||||
})
|
||||
.error(resp => {
|
||||
.catch(resp => {
|
||||
const err = new Error(resp.message);
|
||||
err.stack = resp.stack;
|
||||
notify.error(err);
|
||||
|
@ -26,4 +30,3 @@ const FetchFieldsProvider = (Notifier, $http) => {
|
|||
};
|
||||
|
||||
export { FetchFieldsProvider };
|
||||
|
||||
|
|
|
@ -215,15 +215,18 @@ app.controller('timelion', function (
|
|||
$scope.state.save();
|
||||
$scope.running = true;
|
||||
|
||||
$http.post('../api/timelion/run', {
|
||||
const httpResult = $http.post('../api/timelion/run', {
|
||||
sheet: $scope.state.sheet,
|
||||
time: _.extend(timefilter.time, {
|
||||
interval: $scope.state.interval,
|
||||
timezone: timezone
|
||||
}),
|
||||
})
|
||||
// data, status, headers, config
|
||||
.success(function (resp) {
|
||||
.then(resp => resp.data)
|
||||
.catch(resp => { throw resp.data; });
|
||||
|
||||
httpResult
|
||||
.then(function (resp) {
|
||||
dismissNotifications();
|
||||
$scope.stats = resp.stats;
|
||||
$scope.sheet = resp.sheet;
|
||||
|
@ -234,7 +237,7 @@ app.controller('timelion', function (
|
|||
});
|
||||
$scope.running = false;
|
||||
})
|
||||
.error(function (resp) {
|
||||
.catch(function (resp) {
|
||||
$scope.sheet = [];
|
||||
$scope.running = false;
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ const TimelionRequestHandlerProvider = function (Private, Notifier, $http, $root
|
|||
const expression = vis.params.expression;
|
||||
if (!expression) return;
|
||||
|
||||
$http.post('../api/timelion/run', {
|
||||
const httpResult = $http.post('../api/timelion/run', {
|
||||
sheet: [expression],
|
||||
extended: {
|
||||
es: {
|
||||
|
@ -32,10 +32,14 @@ const TimelionRequestHandlerProvider = function (Private, Notifier, $http, $root
|
|||
timezone: timezone
|
||||
}),
|
||||
})
|
||||
.success(function (resp) {
|
||||
.then(resp => resp.data)
|
||||
.catch(resp => { throw resp.data; });
|
||||
|
||||
httpResult
|
||||
.then(function (resp) {
|
||||
resolve(resp);
|
||||
})
|
||||
.error(function (resp) {
|
||||
.catch(function (resp) {
|
||||
const err = new Error(resp.message);
|
||||
err.stack = resp.stack;
|
||||
notify.error(err);
|
||||
|
|
|
@ -155,7 +155,19 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
|
|||
// This happens if show is triggered multiple times before any hide is triggered.
|
||||
if (!popupTimeout) {
|
||||
popupTimeout = $timeout( show, ttScope.popupDelay, false );
|
||||
popupTimeout.then(function(reposition){reposition();});
|
||||
popupTimeout
|
||||
.then(reposition => reposition())
|
||||
.catch((error) => {
|
||||
// if the timeout is canceled then the string `canceled` is thrown. To prevent
|
||||
// this from triggering an 'unhandled promise rejection' in angular 1.5+ the
|
||||
// $timeout service explicitely tells $q that the promise it generated is "handled"
|
||||
// but that does not include down chain promises like the one created by calling
|
||||
// `popupTimeout.then()`. Because of this we need to ignore the "canceled" string
|
||||
// and only propagate real errors
|
||||
if (error !== 'canceled') {
|
||||
throw error
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
show()();
|
||||
|
|
|
@ -172,7 +172,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
|
|||
//we need to propagate user's query so we can higlight matches
|
||||
scope.query = undefined;
|
||||
|
||||
//Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
|
||||
//Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
|
||||
var timeoutPromise;
|
||||
|
||||
var scheduleSearchWithTimeout = function(inputValue) {
|
||||
|
@ -379,8 +379,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
|
|||
},
|
||||
link:function (scope, element, attrs) {
|
||||
var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html';
|
||||
$http.get(tplUrl, {cache: $templateCache}).success(function(tplContent){
|
||||
element.replaceWith($compile(tplContent.trim())(scope));
|
||||
$http.get(tplUrl, {cache: $templateCache}).then(function(resp){
|
||||
element.replaceWith($compile(resp.data.trim())(scope));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
6
src/ui/public/chrome/api/angular.js
vendored
6
src/ui/public/chrome/api/angular.js
vendored
|
@ -33,11 +33,13 @@ export function initAngularApi(chrome, internals) {
|
|||
return a.href;
|
||||
}()))
|
||||
.config(chrome.$setupXsrfRequestInterceptor)
|
||||
.config(['$compileProvider', function ($compileProvider) {
|
||||
.config(function ($compileProvider, $locationProvider) {
|
||||
if (!internals.devMode) {
|
||||
$compileProvider.debugInfoEnabled(false);
|
||||
}
|
||||
}])
|
||||
|
||||
$locationProvider.hashPrefix('');
|
||||
})
|
||||
.run(($location, $rootScope, Private, config) => {
|
||||
chrome.getFirstPathSegment = () => {
|
||||
return $location.path().split('/')[1];
|
||||
|
|
4
src/ui/public/compat/index.js
Normal file
4
src/ui/public/compat/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
export {
|
||||
InitAfterBindingsWorkaround,
|
||||
callAfterBindingsWorkaround
|
||||
} from './init_after_bindings_workaround';
|
63
src/ui/public/compat/init_after_bindings_workaround.js
Normal file
63
src/ui/public/compat/init_after_bindings_workaround.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* WHAT NEEDS THIS WORKAROUND?
|
||||
* ===========================
|
||||
* Any directive that meets all of the following criteria:
|
||||
* - uses isolate scope bindings
|
||||
* - sets `bindToController: true`
|
||||
* - synchronously accesses the bound values in the controller constructor
|
||||
*
|
||||
*
|
||||
*
|
||||
* HOW DO I GET RID OF IT?
|
||||
* =======================
|
||||
* The quick band-aid solution:
|
||||
* Wrap your constructor logic so it doesn't access bound values
|
||||
* synchronously. This can have subtle bugs which is why I didn't
|
||||
* just wrap all of the offenders in $timeout() and made this
|
||||
* workaround instead.
|
||||
*
|
||||
* The more complete solution:
|
||||
* Use the new component lifecycle methods, like `$onInit()`, to access
|
||||
* bindings immediately after the constructor is called, which shouldn't
|
||||
* have any observable effect outside of the constructor.
|
||||
*
|
||||
* NOTE: `$onInit()` is not dependency injected, if you need controller specific
|
||||
* dependencies like `$scope` then you're probably using watchers and should
|
||||
* take a look at the new one-way data flow facitilies available to
|
||||
* directives/components:
|
||||
*
|
||||
* https://docs.angularjs.org/guide/component#component-based-application-architecture
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
export class InitAfterBindingsWorkaround {
|
||||
static $inject = ['$injector', '$attrs', '$element', '$scope', '$transclude']
|
||||
constructor($injector, $attrs, $element, $scope, $transclude) {
|
||||
if (!this.initAfterBindings) {
|
||||
throw new Error('When using inheritance you must move the logic in the constructor to the `initAfterBindings` method');
|
||||
}
|
||||
|
||||
this.$onInit = () => {
|
||||
$injector.invoke(this.initAfterBindings, this, {
|
||||
$attrs,
|
||||
$element,
|
||||
$scope,
|
||||
$transclude
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function callAfterBindingsWorkaround(constructor) {
|
||||
return function InitAfterBindingsWrapper($injector, $attrs, $element, $scope, $transclude) {
|
||||
this.$onInit = () => {
|
||||
$injector.invoke(constructor, this, {
|
||||
$attrs,
|
||||
$element,
|
||||
$scope,
|
||||
$transclude
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
|
@ -6,7 +6,7 @@ import { ConfigDelayedUpdaterProvider } from 'ui/config/_delayed_updater';
|
|||
const module = uiModules.get('kibana/config');
|
||||
|
||||
// service for delivering config variables to everywhere else
|
||||
module.service(`config`, function (Private, $rootScope, $http, chrome, uiSettings) {
|
||||
module.service(`config`, function (Private, $rootScope, chrome, uiSettings) {
|
||||
const config = this;
|
||||
const notify = new Notifier({ location: `Config` });
|
||||
const { defaults, user: initialUserSettings } = uiSettings;
|
||||
|
|
178
src/ui/public/fancy_forms/__tests__/nested_fancy_forms.js
Normal file
178
src/ui/public/fancy_forms/__tests__/nested_fancy_forms.js
Normal file
|
@ -0,0 +1,178 @@
|
|||
import ngMock from 'ng_mock';
|
||||
import expect from 'expect.js';
|
||||
import sinon from 'sinon';
|
||||
import $ from 'jquery';
|
||||
|
||||
const template = `
|
||||
<form name="person" ng-submit="onSubmit()">
|
||||
<input data-test-subj="name" ng-model="name" required/>
|
||||
<ul>
|
||||
<li ng-repeat="task in tasks">
|
||||
<ng-form data-test-subj="{{'task-' + $index}}">
|
||||
<input data-test-subj="taskName" ng-model="task.name" required />
|
||||
<input data-test-subj="taskDesc" ng-model="task.description" required />
|
||||
</ng-form>
|
||||
</li>
|
||||
</ul>
|
||||
<button data-test-subj="submit" type="submit">Submit</button>
|
||||
</form>
|
||||
`;
|
||||
|
||||
describe('fancy forms', function () {
|
||||
let setup;
|
||||
const trash = [];
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(($injector) => {
|
||||
const $rootScope = $injector.get('$rootScope');
|
||||
const $compile = $injector.get('$compile');
|
||||
|
||||
setup = function (options = {}) {
|
||||
const {
|
||||
name = 'person1',
|
||||
tasks = [],
|
||||
onSubmit = () => {},
|
||||
} = options;
|
||||
|
||||
const $el = $(template).appendTo('body');
|
||||
trash.push(() => $el.remove());
|
||||
const $scope = $rootScope.$new();
|
||||
|
||||
$scope.name = name;
|
||||
$scope.tasks = tasks;
|
||||
$scope.onSubmit = onSubmit;
|
||||
|
||||
$compile($el)($scope);
|
||||
$scope.$apply();
|
||||
|
||||
return {
|
||||
$el,
|
||||
$scope,
|
||||
};
|
||||
};
|
||||
}));
|
||||
|
||||
afterEach(() => trash.splice(0).forEach(fn => fn()));
|
||||
|
||||
describe('nested forms', function () {
|
||||
it('treats new fields as "soft" errors', function () {
|
||||
const { $scope } = setup({ name: '' });
|
||||
expect($scope.person.errorCount()).to.be(1);
|
||||
expect($scope.person.softErrorCount()).to.be(0);
|
||||
});
|
||||
|
||||
it('upgrades fields to regular errors on attempted submit', function () {
|
||||
const { $scope, $el } = setup({ name: '' });
|
||||
|
||||
expect($scope.person.errorCount()).to.be(1);
|
||||
expect($scope.person.softErrorCount()).to.be(0);
|
||||
$el.findTestSubject('submit').click();
|
||||
expect($scope.person.errorCount()).to.be(1);
|
||||
expect($scope.person.softErrorCount()).to.be(1);
|
||||
});
|
||||
|
||||
it('prevents submit when there are errors', function () {
|
||||
const onSubmit = sinon.stub();
|
||||
const { $scope, $el } = setup({ name: '', onSubmit });
|
||||
|
||||
expect($scope.person.errorCount()).to.be(1);
|
||||
sinon.assert.notCalled(onSubmit);
|
||||
$el.findTestSubject('submit').click();
|
||||
expect($scope.person.errorCount()).to.be(1);
|
||||
sinon.assert.notCalled(onSubmit);
|
||||
|
||||
$scope.$apply(() => {
|
||||
$scope.name = 'foo';
|
||||
});
|
||||
|
||||
expect($scope.person.errorCount()).to.be(0);
|
||||
sinon.assert.notCalled(onSubmit);
|
||||
$el.findTestSubject('submit').click();
|
||||
expect($scope.person.errorCount()).to.be(0);
|
||||
sinon.assert.calledOnce(onSubmit);
|
||||
});
|
||||
|
||||
it('new fields are no longer soft after blur', function () {
|
||||
const { $scope, $el } = setup({ name: '' });
|
||||
expect($scope.person.softErrorCount()).to.be(0);
|
||||
$el.findTestSubject('name').blur();
|
||||
expect($scope.person.softErrorCount()).to.be(1);
|
||||
});
|
||||
|
||||
it('counts errors/softErrors in sub forms', function () {
|
||||
const { $scope, $el } = setup();
|
||||
|
||||
expect($scope.person.errorCount()).to.be(0);
|
||||
|
||||
$scope.$apply(() => {
|
||||
$scope.tasks = [
|
||||
{
|
||||
name: 'foo',
|
||||
description: ''
|
||||
},
|
||||
{
|
||||
name: 'foo',
|
||||
description: ''
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
expect($scope.person.errorCount()).to.be(2);
|
||||
expect($scope.person.softErrorCount()).to.be(0);
|
||||
|
||||
$el.findTestSubject('taskDesc').first().blur();
|
||||
|
||||
expect($scope.person.errorCount()).to.be(2);
|
||||
expect($scope.person.softErrorCount()).to.be(1);
|
||||
});
|
||||
|
||||
it('only counts down', function () {
|
||||
const { $scope, $el } = setup({
|
||||
tasks: [
|
||||
{
|
||||
name: 'foo',
|
||||
description: ''
|
||||
},
|
||||
{
|
||||
name: 'bar',
|
||||
description: ''
|
||||
},
|
||||
{
|
||||
name: 'baz',
|
||||
description: ''
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// top level form sees 3 errors
|
||||
expect($scope.person.errorCount()).to.be(3);
|
||||
expect($scope.person.softErrorCount()).to.be(0);
|
||||
|
||||
$el.find('ng-form').toArray().forEach((el, i) => {
|
||||
const $task = $(el);
|
||||
const $taskScope = $task.scope();
|
||||
const form = $task.controller('form');
|
||||
|
||||
// sub forms only see one error
|
||||
expect(form.errorCount()).to.be(1);
|
||||
expect(form.softErrorCount()).to.be(0);
|
||||
|
||||
// blurs only count locally
|
||||
$task.findTestSubject('taskDesc').blur();
|
||||
expect(form.softErrorCount()).to.be(1);
|
||||
|
||||
// but parent form see them
|
||||
expect($scope.person.softErrorCount()).to.be(1);
|
||||
|
||||
$taskScope.$apply(() => {
|
||||
$taskScope.task.description = 'valid';
|
||||
});
|
||||
|
||||
expect(form.errorCount()).to.be(0);
|
||||
expect(form.softErrorCount()).to.be(0);
|
||||
expect($scope.person.errorCount()).to.be(2 - i);
|
||||
expect($scope.person.softErrorCount()).to.be(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,41 +1,12 @@
|
|||
import _ from 'lodash';
|
||||
import { KbnFormController } from 'ui/fancy_forms/kbn_form_controller';
|
||||
import { uiModules } from 'ui/modules';
|
||||
|
||||
import { decorateFormController } from './kbn_form_controller';
|
||||
import { decorateModelController } from './kbn_model_controller';
|
||||
|
||||
uiModules
|
||||
.get('kibana')
|
||||
.config(function ($provide) {
|
||||
function decorateDirectiveController(DecorativeController) {
|
||||
return function ($delegate, $injector) {
|
||||
// directive providers are arrays
|
||||
$delegate.forEach(function (directive) {
|
||||
// get metadata about all init fns
|
||||
const chain = [directive.controller, DecorativeController].map(function (fn) {
|
||||
const deps = $injector.annotate(fn);
|
||||
return { deps: deps, fn: _.isArray(fn) ? _.last(fn) : fn };
|
||||
});
|
||||
|
||||
// replace the controller with one that will setup the actual controller
|
||||
directive.controller = function stub() {
|
||||
const allDeps = _.toArray(arguments);
|
||||
return chain.reduce(function (controller, link) {
|
||||
const deps = allDeps.splice(0, link.deps.length);
|
||||
return link.fn.apply(controller, deps) || controller;
|
||||
}, this);
|
||||
};
|
||||
|
||||
// set the deps of our new controller to be the merged deps of every fn
|
||||
directive.controller.$inject = chain.reduce(function (deps, link) {
|
||||
return deps.concat(link.deps);
|
||||
}, []);
|
||||
});
|
||||
|
||||
return $delegate;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
$provide.decorator('formDirective', decorateDirectiveController(KbnFormController));
|
||||
$provide.decorator('ngFormDirective', decorateDirectiveController(KbnFormController));
|
||||
$provide.decorator('formDirective', decorateFormController);
|
||||
$provide.decorator('ngFormDirective', decorateFormController);
|
||||
$provide.decorator('ngModelDirective', decorateModelController);
|
||||
});
|
||||
|
|
|
@ -1,74 +1,78 @@
|
|||
import _ from 'lodash';
|
||||
export function decorateFormController($delegate, $injector) {
|
||||
const [directive] = $delegate;
|
||||
const FormController = directive.controller;
|
||||
|
||||
/**
|
||||
* Extension of Angular's FormController class
|
||||
* that provides helpers for error handling/validation.
|
||||
*
|
||||
* @param {$scope} $scope
|
||||
*/
|
||||
export function KbnFormController($scope, $element) {
|
||||
const self = this;
|
||||
class KbnFormController extends FormController {
|
||||
// prevent inheriting FormController's static $inject property
|
||||
// which is angular's cache of the DI arguments for a function
|
||||
static $inject = ['$scope', '$element'];
|
||||
|
||||
self.errorCount = function () {
|
||||
return self.$$invalidModels().length;
|
||||
};
|
||||
constructor($scope, $element, ...superArgs) {
|
||||
super(...superArgs);
|
||||
|
||||
// same as error count, but filters out untouched and pristine models
|
||||
self.softErrorCount = function () {
|
||||
return self.$$invalidModels(function (model) {
|
||||
return model.$touched || model.$dirty;
|
||||
}).length;
|
||||
};
|
||||
const onSubmit = () => {
|
||||
this._markInvalidTouched();
|
||||
};
|
||||
|
||||
self.describeErrors = function () {
|
||||
const count = self.softErrorCount();
|
||||
return count + ' Error' + (count === 1 ? '' : 's');
|
||||
};
|
||||
|
||||
self.$$invalidModels = function (predicate) {
|
||||
predicate = _.callback(predicate);
|
||||
|
||||
const invalid = [];
|
||||
|
||||
_.forOwn(self.$error, function collect(models) {
|
||||
if (!models) return;
|
||||
|
||||
models.forEach(function (model) {
|
||||
if (model.$$invalidModels) {
|
||||
// recurse into child form
|
||||
_.forOwn(model.$error, collect);
|
||||
} else {
|
||||
if (predicate(model)) {
|
||||
// prevent dups
|
||||
let len = invalid.length;
|
||||
while (len--) if (invalid[len] === model) return;
|
||||
|
||||
invalid.push(model);
|
||||
}
|
||||
}
|
||||
$element.on('submit', onSubmit);
|
||||
$scope.$on('$destroy', () => {
|
||||
$element.off('submit', onSubmit);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return invalid;
|
||||
};
|
||||
errorCount() {
|
||||
return this._getInvalidModels().length;
|
||||
}
|
||||
|
||||
self.$setTouched = function () {
|
||||
self.$$invalidModels().forEach(function (model) {
|
||||
// only kbnModels have $setTouched
|
||||
if (model.$setTouched) model.$setTouched();
|
||||
});
|
||||
};
|
||||
// same as error count, but filters out untouched and pristine models
|
||||
softErrorCount() {
|
||||
return this._getInvalidModels()
|
||||
.filter(model => model.$touched || model.$dirty)
|
||||
.length;
|
||||
}
|
||||
|
||||
function filterSubmits(event) {
|
||||
if (self.errorCount()) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
self.$setTouched();
|
||||
describeErrors() {
|
||||
const count = this.softErrorCount();
|
||||
return `${count} Error${count === 1 ? '' : 's'}`;
|
||||
}
|
||||
|
||||
$setTouched() {
|
||||
this._getInvalidModels()
|
||||
.forEach(model => model.$setTouched());
|
||||
}
|
||||
|
||||
_markInvalidTouched(event) {
|
||||
if (this.errorCount()) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
this.$setTouched();
|
||||
}
|
||||
}
|
||||
|
||||
_getInvalidModels() {
|
||||
return this.$$controls.reduce((acc, control) => {
|
||||
// recurse into sub-form
|
||||
if (typeof control._getInvalidModels === 'function') {
|
||||
return [...acc, ...control._getInvalidModels()];
|
||||
}
|
||||
|
||||
if (control.$invalid) {
|
||||
return [...acc, control];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
|
||||
$element.on('submit', filterSubmits);
|
||||
$scope.$on('$destroy', function () {
|
||||
$element.off('submit', filterSubmits);
|
||||
});
|
||||
// replace controller with our wrapper
|
||||
directive.controller = [
|
||||
...$injector.annotate(KbnFormController),
|
||||
...$injector.annotate(FormController),
|
||||
(...args) => (
|
||||
new KbnFormController(...args)
|
||||
)
|
||||
];
|
||||
|
||||
return $delegate;
|
||||
}
|
||||
|
|
37
src/ui/public/fancy_forms/kbn_model_controller.js
Normal file
37
src/ui/public/fancy_forms/kbn_model_controller.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
export function decorateModelController($delegate, $injector) {
|
||||
const [directive] = $delegate;
|
||||
const ModelController = directive.controller;
|
||||
|
||||
class KbnModelController extends ModelController {
|
||||
// prevent inheriting ModelController's static $inject property
|
||||
// which is angular's cache of the DI arguments for a function
|
||||
static $inject = ['$scope', '$element'];
|
||||
|
||||
constructor($scope, $element, ...superArgs) {
|
||||
super(...superArgs);
|
||||
|
||||
const onInvalid = () => {
|
||||
this.$setTouched();
|
||||
};
|
||||
|
||||
// the browser emits an "invalid" event when browser supplied
|
||||
// validation fails, which implies that the user has indirectly
|
||||
// interacted with the control and it should be treated as "touched"
|
||||
$element.on('invalid', onInvalid);
|
||||
$scope.$on('$destroy', () => {
|
||||
$element.off('invalid', onInvalid);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// replace controller with our wrapper
|
||||
directive.controller = [
|
||||
...$injector.annotate(KbnModelController),
|
||||
...$injector.annotate(ModelController),
|
||||
(...args) => (
|
||||
new KbnModelController(...args)
|
||||
)
|
||||
];
|
||||
|
||||
return $delegate;
|
||||
}
|
|
@ -19,7 +19,7 @@ uiModules
|
|||
attrs.$observe('id', () => $scope.id = attrs.id);
|
||||
|
||||
// bind our local model with the outside ngModel
|
||||
$scope.$watch('model', ngModelCntrl.$setViewValue);
|
||||
$scope.$watch('model', v => ngModelCntrl.$setViewValue(v));
|
||||
ngModelCntrl.$render = function () {
|
||||
$scope.model = ngModelCntrl.$viewValue;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import _ from 'lodash';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { callAfterBindingsWorkaround } from 'ui/compat';
|
||||
import { FILTER_OPERATOR_TYPES } from './lib/filter_operators';
|
||||
import template from './filter_editor.html';
|
||||
import { documentationLinks } from '../documentation_links/documentation_links';
|
||||
|
@ -33,7 +34,7 @@ module.directive('filterEditor', function ($timeout, indexPatterns) {
|
|||
},
|
||||
controllerAs: 'filterEditor',
|
||||
bindToController: true,
|
||||
controller: function ($scope, $element) {
|
||||
controller: callAfterBindingsWorkaround(function ($scope, $element) {
|
||||
this.init = () => {
|
||||
const { filter } = this;
|
||||
this.docLinks = documentationLinks;
|
||||
|
@ -124,6 +125,6 @@ module.directive('filterEditor', function ($timeout, indexPatterns) {
|
|||
$timeout(() => this.onCancel());
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
|
|
@ -26,7 +26,7 @@ export function getRoutes() {
|
|||
};
|
||||
}
|
||||
|
||||
export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise, confirmModalPromise, kbnUrl) {
|
||||
export function IndexPatternProvider(Private, config, Promise, confirmModalPromise, kbnUrl) {
|
||||
const fieldformats = Private(RegistryFieldFormatsProvider);
|
||||
const getConfig = (...args) => config.get(...args);
|
||||
const getIds = Private(IndexPatternsGetProvider)('id');
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import angular from 'angular';
|
||||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import sinon from 'sinon';
|
||||
import $ from 'jquery';
|
||||
|
||||
describe('ui/modals/confirm_modal_promise', function () {
|
||||
|
||||
|
@ -23,11 +23,8 @@ describe('ui/modals/confirm_modal_promise', function () {
|
|||
});
|
||||
|
||||
afterEach(function () {
|
||||
const confirmButton = angular.element(document.body).find('[data-test-subj=confirmModalConfirmButton]');
|
||||
if (confirmButton) {
|
||||
$rootScope.$digest();
|
||||
angular.element(confirmButton).click();
|
||||
}
|
||||
$rootScope.$digest();
|
||||
$.findTestSubject('confirmModalConfirmButton').click();
|
||||
});
|
||||
|
||||
describe('before timeout completes', function () {
|
||||
|
@ -42,7 +39,7 @@ describe('ui/modals/confirm_modal_promise', function () {
|
|||
describe('after timeout completes', function () {
|
||||
it('confirmation dialogue is loaded to dom with message', function () {
|
||||
$rootScope.$digest();
|
||||
const confirmModalElement = angular.element(document.body).find('[data-test-subj=confirmModal]');
|
||||
const confirmModalElement = $.findTestSubject('confirmModal');
|
||||
expect(confirmModalElement).to.not.be(undefined);
|
||||
|
||||
const htmlString = confirmModalElement[0].innerHTML;
|
||||
|
@ -57,7 +54,7 @@ describe('ui/modals/confirm_modal_promise', function () {
|
|||
|
||||
promise.then(confirmCallback, cancelCallback);
|
||||
$rootScope.$digest();
|
||||
const confirmButton = angular.element(document.body).find('[data-test-subj=confirmModalConfirmButton]');
|
||||
const confirmButton = $.findTestSubject('confirmModalConfirmButton');
|
||||
|
||||
confirmButton.click();
|
||||
expect(confirmCallback.called).to.be(true);
|
||||
|
@ -72,7 +69,7 @@ describe('ui/modals/confirm_modal_promise', function () {
|
|||
promise.then(confirmCallback, cancelCallback);
|
||||
|
||||
$rootScope.$digest();
|
||||
const noButton = angular.element(document.body).find('[data-test-subj=confirmModalCancelButton]');
|
||||
const noButton = $.findTestSubject('confirmModalCancelButton');
|
||||
noButton.click();
|
||||
|
||||
expect(cancelCallback.called).to.be(true);
|
||||
|
@ -82,12 +79,17 @@ describe('ui/modals/confirm_modal_promise', function () {
|
|||
|
||||
describe('error is thrown', function () {
|
||||
it('when no confirm button text is used', function () {
|
||||
try {
|
||||
confirmModalPromise(message);
|
||||
expect(false).to.be(true);
|
||||
} catch (error) {
|
||||
expect(error).to.not.be(undefined);
|
||||
}
|
||||
const confirmCallback = sinon.spy();
|
||||
const cancelCallback = sinon.spy();
|
||||
confirmModalPromise(message).then(confirmCallback, cancelCallback);
|
||||
|
||||
$rootScope.$digest();
|
||||
sinon.assert.notCalled(confirmCallback);
|
||||
sinon.assert.calledOnce(cancelCallback);
|
||||
sinon.assert.calledWithExactly(
|
||||
cancelCallback,
|
||||
sinon.match.has('message', sinon.match(/confirmation button text/))
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { uiModules } from 'ui/modules';
|
||||
import { callAfterBindingsWorkaround } from 'ui/compat';
|
||||
import template from './pattern_checker.html';
|
||||
import './pattern_checker.less';
|
||||
import chrome from 'ui/chrome';
|
||||
|
@ -14,7 +15,7 @@ module.directive('patternChecker', function () {
|
|||
scope: {
|
||||
pattern: '='
|
||||
},
|
||||
controller: function (Notifier, $scope, $timeout, $http) {
|
||||
controller: callAfterBindingsWorkaround(function (Notifier, $scope, $timeout, $http) {
|
||||
let validationTimeout;
|
||||
|
||||
const notify = new Notifier({
|
||||
|
@ -46,7 +47,6 @@ module.directive('patternChecker', function () {
|
|||
});
|
||||
|
||||
this.validateInstall();
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { uiModules } from 'ui/modules';
|
||||
import { callAfterBindingsWorkaround } from 'ui/compat';
|
||||
import template from './query_bar.html';
|
||||
import { queryLanguages } from '../lib/queryLanguages';
|
||||
import { documentationLinks } from '../../documentation_links/documentation_links.js';
|
||||
|
@ -17,7 +18,7 @@ module.directive('queryBar', function () {
|
|||
},
|
||||
controllerAs: 'queryBar',
|
||||
bindToController: true,
|
||||
controller: function ($scope, config) {
|
||||
controller: callAfterBindingsWorkaround(function ($scope, config) {
|
||||
this.queryDocLinks = documentationLinks.query;
|
||||
this.appName = this.appName || 'global';
|
||||
this.availableQueryLanguages = queryLanguages;
|
||||
|
@ -36,7 +37,7 @@ module.directive('queryBar', function () {
|
|||
$scope.$watch('queryBar.query', (newQuery) => {
|
||||
this.localQuery = Object.assign({}, newQuery);
|
||||
}, true);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
});
|
||||
|
|
|
@ -112,7 +112,10 @@ describe('typeahead directive', function () {
|
|||
describe('controller scope', function () {
|
||||
it('should contain the input model', function () {
|
||||
expect($typeaheadScope.inputModel).to.be.an('object');
|
||||
expect($typeaheadScope.inputModel).to.have.keys(['$viewValue', '$modelValue', '$setViewValue']);
|
||||
expect($typeaheadScope.inputModel)
|
||||
.to.have.property('$viewValue')
|
||||
.and.have.property('$modelValue')
|
||||
.and.have.property('$setViewValue').a('function');
|
||||
});
|
||||
|
||||
it('should save data to the scope', function () {
|
||||
|
|
|
@ -12,7 +12,7 @@ import visOptionsTemplate from './vis_options.html';
|
|||
|
||||
uiModules
|
||||
.get('app/visualize')
|
||||
.directive('visEditorVisOptions', function (Private, $timeout, $compile) {
|
||||
.directive('visEditorVisOptions', function (Private, $compile) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: visOptionsTemplate,
|
||||
|
|
|
@ -40,9 +40,9 @@ describe('$scope.$watchMulti', function () {
|
|||
expect(triggers).to.be(2);
|
||||
|
||||
// remove watchers
|
||||
expect($scope.$$watchers).to.not.eql([]);
|
||||
expect($scope.$$watchers).to.not.have.length(0);
|
||||
unwatch();
|
||||
expect($scope.$$watchers).to.eql([]);
|
||||
expect($scope.$$watchers).to.have.length(0);
|
||||
|
||||
// prove that it doesn't trigger anymore
|
||||
$scope.a++;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue