IGNITE-11338 Web Console: Fixed edit mode of "list-editable" component.
diff --git a/frontend/app/components/list-editable/components/list-editable-add-item-button/component.js b/frontend/app/components/list-editable/components/list-editable-add-item-button/component.ts
similarity index 79%
rename from frontend/app/components/list-editable/components/list-editable-add-item-button/component.js
rename to frontend/app/components/list-editable/components/list-editable-add-item-button/component.ts
index 84ee1e8..793270f 100644
--- a/frontend/app/components/list-editable/components/list-editable-add-item-button/component.js
+++ b/frontend/app/components/list-editable/components/list-editable-add-item-button/component.ts
@@ -22,33 +22,20 @@
 
 /**
  * Adds "add new item" button to list-editable-no-items slot and after list-editable
- * @type {ng.IComponentController}
  */
-export class ListEditableAddItemButton {
+export class ListEditableAddItemButton<T> {
     /** 
      * Template for button that's inserted after list-editable
-     * @type {string}
      */
-    static hasItemsTemplate = hasItemsTemplate;
-    /** @type {ListEditable} */
-    _listEditable;
-    /** @type {string} */
-    labelSingle;
-    /** @type {string} */
-    labelMultiple;
-    /** @type {ng.ICompiledExpression} */
-    _addItem;
+    static hasItemsTemplate: string = hasItemsTemplate;
+    _listEditable: ListEditable<T>;
+    labelSingle: string;
+    labelMultiple: string;
+    _addItem: ng.ICompiledExpression;
 
     static $inject = ['$compile', '$scope'];
 
-    /**
-     * @param {ng.ICompileService} $compile
-     * @param {ng.IScope} $scope
-     */
-    constructor($compile, $scope) {
-        this.$compile = $compile;
-        this.$scope = $scope;
-    }
+    constructor(private $compile: ng.ICompileService, private $scope: ng.IScope) {}
 
     $onDestroy() {
         this._listEditable = this._hasItemsButton = null;
diff --git a/frontend/app/components/list-editable/components/list-editable-add-item-button/index.js b/frontend/app/components/list-editable/components/list-editable-add-item-button/index.ts
similarity index 100%
rename from frontend/app/components/list-editable/components/list-editable-add-item-button/index.js
rename to frontend/app/components/list-editable/components/list-editable-add-item-button/index.ts
diff --git a/frontend/app/components/list-editable/components/list-editable-one-way/directive.js b/frontend/app/components/list-editable/components/list-editable-one-way/directive.js
deleted file mode 100644
index 320791b..0000000
--- a/frontend/app/components/list-editable/components/list-editable-one-way/directive.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import isMatch from 'lodash/isMatch';
-import {default as ListEditableController} from '../../controller';
-
-/** @type {ng.IDirectiveFactory} */
-export default function listEditableOneWay() {
-    return {
-        require: {
-            list: 'listEditable'
-        },
-        bindToController: {
-            onItemChange: '&?',
-            onItemRemove: '&?'
-        },
-        controller: class Controller {
-            /** @type {ListEditableController} */
-            list;
-            /** @type {ng.ICompiledExpression} onItemChange */
-            onItemChange;
-            /** @type {ng.ICompiledExpression} onItemRemove */
-            onItemRemove;
-
-            static $inject = ['$scope'];
-            /**
-             * @param {ng.IScope} $scope
-             */
-            constructor($scope) {
-                this.$scope = $scope;
-            }
-            $onInit() {
-                this.list.save = (item, index) => {
-                    if (!isMatch(this.list.ngModel.$viewValue[index], item)) this.onItemChange({$event: item});
-                };
-                this.list.remove = (index) => this.onItemRemove({$event: this.list.ngModel.$viewValue[index]});
-            }
-        }
-    };
-}
diff --git a/frontend/app/components/list-editable/components/list-editable-one-way/directive.ts b/frontend/app/components/list-editable/components/list-editable-one-way/directive.ts
new file mode 100644
index 0000000..91b0441
--- /dev/null
+++ b/frontend/app/components/list-editable/components/list-editable-one-way/directive.ts
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import isMatch from 'lodash/isMatch';
+import {default as ListEditableController, ID} from '../../controller';
+
+export default function listEditableOneWay(): ng.IDirective {
+    return {
+        require: {
+            list: 'listEditable'
+        },
+        bindToController: {
+            onItemChange: '&?',
+            onItemRemove: '&?'
+        },
+        controller: class Controller<T> {
+            list: ListEditableController<T>;
+            onItemChange: ng.ICompiledExpression;
+            onItemRemove: ng.ICompiledExpression;
+
+            $onInit() {
+                this.list.save = (item: T, id: ID) => {
+                    if (!isMatch(this.list.getItem(id), item)) this.onItemChange({$event: item});
+                };
+                this.list.remove = (id: ID) => this.onItemRemove({
+                    $event: this.list.getItem(id)
+                });
+            }
+        }
+    };
+}
diff --git a/frontend/app/components/list-editable/components/list-editable-one-way/index.js b/frontend/app/components/list-editable/components/list-editable-one-way/index.ts
similarity index 100%
rename from frontend/app/components/list-editable/components/list-editable-one-way/index.js
rename to frontend/app/components/list-editable/components/list-editable-one-way/index.ts
diff --git a/frontend/app/components/list-editable/components/list-editable-save-on-changes/directives.js b/frontend/app/components/list-editable/components/list-editable-save-on-changes/directives.ts
similarity index 77%
rename from frontend/app/components/list-editable/components/list-editable-save-on-changes/directives.js
rename to frontend/app/components/list-editable/components/list-editable-save-on-changes/directives.ts
index f408647..b3cab9e 100644
--- a/frontend/app/components/list-editable/components/list-editable-save-on-changes/directives.js
+++ b/frontend/app/components/list-editable/components/list-editable-save-on-changes/directives.ts
@@ -15,21 +15,17 @@
  * limitations under the License.
  */
 
-import {default as ListEditableController} from '../../controller';
+import {default as ListEditableController, ID, ItemScope} from '../../controller';
+import {ListEditableTransclude} from '../list-editable-transclude/directive';
 
 const CUSTOM_EVENT_TYPE = '$ngModel.change';
 
 /** 
  * Emits $ngModel.change event on every ngModel.$viewValue change
- * @type {ng.IDirectiveFactory}
  */
-export function ngModel() {
+export function ngModel<T>(): ng.IDirective {
     return {
-        /**
-         * @param {JQLite} el
-         * @param {ng.INgModelController} ngModel
-         */
-        link(scope, el, attr, {ngModel, list}) {
+        link(scope, el, attr, {ngModel, list}: {ngModel: ng.INgModelController, list?: ListEditableController<T>}) {
             if (!list)
                 return;
 
@@ -45,17 +41,10 @@
 }
 /** 
  * Triggers $ctrl.save when any ngModel emits $ngModel.change event
- * @type {ng.IDirectiveFactory}
  */
-export function listEditableTransclude() {
+export function listEditableTransclude<T>(): ng.IDirective {
     return {
-        /**
-         * @param {ng.IScope} scope
-         * @param {JQLite} el
-         * @param {ng.IAttributes} attr
-         * @param {ListEditableController} list
-         */
-        link(scope, el, attr, {list, transclude}) {
+        link(scope: ItemScope<T>, el, attr, {list, transclude}: {list?: ListEditableController<T>, transclude: ListEditableTransclude<T>}) {
             if (attr.listEditableTransclude !== 'itemEdit')
                 return;
 
@@ -65,7 +54,7 @@
             let listener = (e) => {
                 e.stopPropagation();
                 scope.$evalAsync(() => {
-                    if (scope.form.$valid) list.save(scope.item, transclude.$index);
+                    if (scope.form.$valid) list.save(scope.item, list.id(scope.item, transclude.$index));
                 });
             };
 
diff --git a/frontend/app/components/list-editable/components/list-editable-save-on-changes/index.js b/frontend/app/components/list-editable/components/list-editable-save-on-changes/index.ts
similarity index 100%
rename from frontend/app/components/list-editable/components/list-editable-save-on-changes/index.js
rename to frontend/app/components/list-editable/components/list-editable-save-on-changes/index.ts
diff --git a/frontend/app/components/list-editable/components/list-editable-transclude/directive.js b/frontend/app/components/list-editable/components/list-editable-transclude/directive.ts
similarity index 88%
rename from frontend/app/components/list-editable/components/list-editable-transclude/directive.js
rename to frontend/app/components/list-editable/components/list-editable-transclude/directive.ts
index 36750ae..272a423 100644
--- a/frontend/app/components/list-editable/components/list-editable-transclude/directive.js
+++ b/frontend/app/components/list-editable/components/list-editable-transclude/directive.ts
@@ -15,38 +15,29 @@
  * limitations under the License.
  */
 
-// eslint-disable-next-line
-import {default as ListEditable} from '../../controller';
+import {default as ListEditable, ItemScope} from '../../controller';
+
+type TranscludedScope<T> = {$form: ng.IFormController, $item?: T} & ng.IScope
 
 /**
  * Transcludes list-editable slots and proxies item and form scope values to the slot scope,
  * also provides a way to retrieve internal list-editable ng-repeat $index by controller getter.
  * User can provide an alias for $item by setting item-name attribute on transclusion slot element.
  */
-export class ListEditableTransclude {
+export class ListEditableTransclude<T> {
     /**
      * Transcluded slot name.
-     *
-     * @type {string}
      */
-    slot;
+    slot: string;
 
-    /**
-     * List-editable controller.
-     *
-     * @type {ListEditable}
-     */
-    list;
+    list: ListEditable<T>;
 
     static $inject = ['$scope', '$element'];
 
-    constructor($scope, $element) {
-        this.$scope = $scope;
-        this.$element = $element;
-    }
+    constructor(private $scope: ItemScope<T>, private $element: JQLite) {}
 
     $postLink() {
-        this.list.$transclude((clone, transcludedScope) => {
+        this.list.$transclude((clone, transcludedScope: TranscludedScope<T>) => {
             // Ilya Borisov: at first I tried to use a slave directive to get value from
             // attribute and set it to ListEditableTransclude controller, but it turns out
             // this directive would run after list-editable-transclude, so that approach
@@ -98,8 +89,6 @@
 
     /**
      * Returns list-editable ng-repeat $index.
-     *
-     * @returns {number}
      */
     get $index() {
         if (!this.$scope)
diff --git a/frontend/app/components/list-editable/components/list-editable-transclude/index.js b/frontend/app/components/list-editable/components/list-editable-transclude/index.ts
similarity index 100%
rename from frontend/app/components/list-editable/components/list-editable-transclude/index.js
rename to frontend/app/components/list-editable/components/list-editable-transclude/index.ts
diff --git a/frontend/app/components/list-editable/controller.js b/frontend/app/components/list-editable/controller.js
deleted file mode 100644
index d1b8900..0000000
--- a/frontend/app/components/list-editable/controller.js
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import _ from 'lodash';
-
-/** @type {ng.IComponentController} */
-export default class {
-    /** @type {ng.INgModelController} */
-    ngModel;
-
-    static $inject = ['$animate', '$element', '$transclude', '$timeout'];
-
-    /**
-     * @param {ng.animate.IAnimateService} $animate
-     * @param {JQLite} $element
-     * @param {ng.ITranscludeFunction} $transclude
-     * @param {ng.ITimeoutService} $timeout
-     */
-    constructor($animate, $element, $transclude, $timeout) {
-        $animate.enabled($element, false);
-        this.$transclude = $transclude;
-        this.$element = $element;
-        this.$timeout = $timeout;
-        this.hasItemView = $transclude.isSlotFilled('itemView');
-
-        this._cache = {};
-    }
-
-    $index(item, $index) {
-        if (item._id)
-            return item._id;
-
-        return $index;
-    }
-
-    $onDestroy() {
-        this.$element = null;
-    }
-
-    $onInit() {
-        this.ngModel.$isEmpty = (value) => {
-            return !Array.isArray(value) || !value.length;
-        };
-        this.ngModel.editListItem = (item) => {
-            this.$timeout(() => {
-                this.startEditView(this.ngModel.$viewValue.indexOf(item));
-                // For some reason required validator does not re-run after adding an item,
-                // the $validate call fixes the issue.
-                this.ngModel.$validate();
-            });
-        };
-        this.ngModel.editListIndex = (index) => {
-            this.$timeout(() => {
-                this.startEditView(index);
-                // For some reason required validator does not re-run after adding an item,
-                // the $validate call fixes the issue.
-                this.ngModel.$validate();
-            });
-        };
-    }
-
-    save(data, idx) {
-        this.ngModel.$setViewValue(this.ngModel.$viewValue.map((v, i) => i === idx ? _.cloneDeep(data) : v));
-    }
-
-    revert(idx) {
-        delete this._cache[idx];
-    }
-
-    remove(idx) {
-        this.ngModel.$setViewValue(this.ngModel.$viewValue.filter((v, i) => i !== idx));
-    }
-
-    isEditView(idx) {
-        return this._cache.hasOwnProperty(idx);
-    }
-
-    getEditView(idx) {
-        return this._cache[idx];
-    }
-
-    startEditView(idx) {
-        this._cache[idx] = _.cloneDeep(this.ngModel.$viewValue[idx]);
-    }
-
-    stopEditView(data, idx, form) {
-        // By default list-editable saves only valid values, but if you specify {allowInvalid: true}
-        // ng-model-option, then it will always save. Be careful and pay extra attention to validation
-        // when doing so, it's an easy way to miss invalid values this way.
-
-        // Don't close if form is invalid and allowInvalid is turned off (which is default value)
-        if (!form.$valid && !this.ngModel.$options.getOption('allowInvalid'))
-            return;
-
-        delete this._cache[idx];
-
-        this.save(data, idx);
-    }
-}
diff --git a/frontend/app/components/list-editable/controller.ts b/frontend/app/components/list-editable/controller.ts
new file mode 100644
index 0000000..e870e82
--- /dev/null
+++ b/frontend/app/components/list-editable/controller.ts
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import _ from 'lodash';
+
+export interface ListEditableNgModel<T> extends ng.INgModelController {
+    $viewValue: T[],
+    editListItem(item: T): void,
+    editListIndex(index: number): void
+}
+
+export type ID = (string | number) & {tag: 'ItemID'}
+
+export type ItemScope<T> = {$index: number, item: T, form: ng.IFormController} & ng.IScope
+
+export default class ListEditable<T extends {_id?: any}> {
+    static $inject = ['$animate', '$element', '$transclude', '$timeout'];
+
+    constructor(
+        $animate: ng.animate.IAnimateService,
+        public $element: JQLite,
+        public $transclude: ng.ITranscludeFunction,
+        private $timeout: ng.ITimeoutService
+    ) {
+        $animate.enabled($element, false);
+        this.hasItemView = $transclude.isSlotFilled('itemView');
+
+        this._cache = new Map();
+    }
+
+    ngModel: ListEditableNgModel<T>;
+    hasItemView: boolean
+    private _cache: Map<ID, T>
+
+    id(item: T | undefined, index: number): ID {
+        if (item && item._id)
+            return item._id as ID;
+
+        return index as ID;
+    }
+
+    $onDestroy() {
+        this.$element = null;
+    }
+
+    $onInit() {
+        this.ngModel.$isEmpty = (value) => {
+            return !Array.isArray(value) || !value.length;
+        };
+        this.ngModel.editListItem = (item) => {
+            this.$timeout(() => {
+                this.startEditView(this.id(item, this.ngModel.$viewValue.indexOf(item)));
+                // For some reason required validator does not re-run after adding an item,
+                // the $validate call fixes the issue.
+                this.ngModel.$validate();
+            });
+        };
+        this.ngModel.editListIndex = (index) => {
+            this.$timeout(() => {
+                this.startEditView(this.id(this.ngModel.$viewValue[index], index));
+                // For some reason required validator does not re-run after adding an item,
+                // the $validate call fixes the issue.
+                this.ngModel.$validate();
+            });
+        };
+    }
+
+    save(item: T, id: ID) {
+        this.ngModel.$setViewValue(
+            this.ngModel.$viewValue.map((v, i) => this.id(v, i) === id ? _.cloneDeep(item) : v)
+        );
+    }
+
+    remove(id: ID): void {
+        this.ngModel.$setViewValue(this.ngModel.$viewValue.filter((v, i) => this.id(v, i) !== id));
+    }
+
+    isEditView(id: ID): boolean {
+        return this._cache.has(id);
+    }
+
+    getEditView(id: ID): T {
+        return this._cache.get(id);
+    }
+
+    getItem(id: ID): T {
+        return this.ngModel.$viewValue.find((v, i) => this.id(v, i) === id);
+    }
+
+    startEditView(id: ID) {
+        this._cache.set(
+            id,
+            _.cloneDeep(this.getItem(id))
+        );
+    }
+
+    stopEditView(data: T, id: ID, form: ng.IFormController) {
+        // By default list-editable saves only valid values, but if you specify {allowInvalid: true}
+        // ng-model-option, then it will always save. Be careful and pay extra attention to validation
+        // when doing so, it's an easy way to miss invalid values this way.
+
+        // Don't close if form is invalid and allowInvalid is turned off (which is default value)
+        if (!form.$valid && !this.ngModel.$options.getOption('allowInvalid'))
+            return;
+
+        this._cache.delete(id);
+
+        this.save(data, id);
+    }
+}
diff --git a/frontend/app/components/list-editable/index.js b/frontend/app/components/list-editable/index.ts
similarity index 100%
rename from frontend/app/components/list-editable/index.js
rename to frontend/app/components/list-editable/index.ts
diff --git a/frontend/app/components/list-editable/template.pug b/frontend/app/components/list-editable/template.pug
index b52bfd2..19a9507 100644
--- a/frontend/app/components/list-editable/template.pug
+++ b/frontend/app/components/list-editable/template.pug
@@ -16,9 +16,9 @@
 
 .le-body
     .le-row(
-        ng-repeat='item in $ctrl.ngModel.$viewValue track by $ctrl.$index(item, $index)'
+        ng-repeat='item in $ctrl.ngModel.$viewValue track by $ctrl.id(item, $index)'
         ng-class=`{
-            'le-row--editable': $ctrl.isEditView($index),
+            'le-row--editable': $ctrl.isEditView($ctrl.id(item, $index)),
             'le-row--has-item-view': $ctrl.hasItemView
         }`)
 
@@ -30,20 +30,20 @@
             span {{ $index+1 }}
 
         .le-row-item
-            .le-row-item-view(ng-if='$ctrl.hasItemView && !$ctrl.isEditView($index)' ng-click='$ctrl.startEditView($index);')
+            .le-row-item-view(ng-if='$ctrl.hasItemView && !$ctrl.isEditView($ctrl.id(item, $index))' ng-click='$ctrl.startEditView($ctrl.id(item, $index))')
                 div(list-editable-transclude='itemView')
             div(
-                ng-if='!$ctrl.hasItemView || $ctrl.isEditView($index)'
-                ignite-on-focus-out='$ctrl.stopEditView(item, $index, form);'
+                ng-if='!$ctrl.hasItemView || $ctrl.isEditView($ctrl.id(item, $index))'
+                ignite-on-focus-out='$ctrl.stopEditView(item, $ctrl.id(item, $index), form)'
                 ignite-on-focus-out-ignored-classes='bssm-click-overlay bssm-item-text bssm-item-button'
             )
-                .le-row-item-view(ng-show='$ctrl.hasItemView' ng-init='$ctrl.startEditView($index);item = $ctrl.getEditView($index);')
+                .le-row-item-view(ng-show='$ctrl.hasItemView' ng-init='$ctrl.startEditView($ctrl.id(item, $index));item = $ctrl.getEditView($ctrl.id(item, $index))')
                     div(list-editable-transclude='itemView')
                 .le-row-item-edit(ng-form name='form')
                     div(list-editable-transclude='itemEdit')
 
         .le-row-cross
-            button.btn-ignite.btn-ignite--link-dashed-secondary(type='button' ng-click='$ctrl.remove($index)')
+            button.btn-ignite.btn-ignite--link-dashed-secondary(type='button' ng-click='$ctrl.remove($ctrl.id(item, $index))')
                 svg(ignite-icon='cross')
 
     .le-row(ng-hide='$ctrl.ngModel.$viewValue.length')