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')