Rewrite WB Action->Base to use @ref facility

Now all standard actions are put into top-level
Barricade object in WB controller and then Base field
just fetches id-s from them (and we use them same 
standardActions top-level property for resetting a 
'Base Input' field with a list of keys corresponding 
to a specific standardAction.

Also new unit-tests (for filters and for dictionary
Merlin model) are added.

Change-Id: Ieb6e9330db8fbeb83e4f0f2a64611e1b6b31006c
Closes-Bug: #1467511
This commit is contained in:
Timur Sufiev 2015-06-24 12:16:35 -07:00
parent d5d9321eca
commit 7ccf4f0dd3
8 changed files with 286 additions and 82 deletions

View File

@ -20,6 +20,14 @@
} else {
$scope.workbook = models.Workbook.create({name: 'My Workbook'});
}
$scope.root = models.Root.create();
$scope.root.set('workbook', $scope.workbook);
$scope.root.set('standardActions', {
'nova.create_server': ['image', 'flavor', 'network_id'],
'neutron.create_network': ['name', 'create_subnet'],
'glance.create_image': ['image_url']
});
};
function getNextIDSuffix(container, regexp) {

View File

@ -108,13 +108,13 @@
var self = fields.frozendict.create.call(this, json, parameters),
base = self.get('base');
base.on('change', function(operation) {
var baseValue;
var argsEntry, pos, entry;
if ( operation != 'id' ) {
baseValue = base.get();
if ( baseValue ) {
base.getSchema(baseValue).then(function(keys) {
self.get('base-input').setSchema(keys);
});
pos = base._stdActions.getPosByID(base.get());
if ( pos > -1 ) {
entry = self.get('base-input');
argsEntry = base._stdActions.get(pos);
entry.resetKeys(argsEntry.toJSON());
}
}
});
@ -125,44 +125,66 @@
'@class': fields.string.extend({
create: function(json, parameters) {
var self = fields.string.create.call(this, json, parameters),
schema = {},
url = utils.getMeta(self, 'autocompletionUrl');
stdActionsCls = Barricade.create({
'@type': String,
'@ref': {
to: function() {
return fields.StandardActions;
},
needs: function() {
return models.Root;
},
getter: function(data) {
return data.needed.get('standardActions');
}
}
});
self.getSchema = function(key) {
var deferred = $q.defer();
if ( !(key in schema) ) {
$http.get(url+'?key='+key).success(function(keys) {
schema[key] = keys;
deferred.resolve(keys);
}).error(function() {
deferred.reject();
self._stdActions = stdActionsCls.create().on(
'replace', function(newValue) {
self._stdActions = newValue;
self._stdActions.on('change', function() {
self._choices = self._stdActions.getIDs();
self.resetValues();
});
} else {
deferred.resolve(schema[key]);
}
return deferred.promise;
};
self._stdActions.emit('change');
});
return self;
}
},
_choices: []
}, {
'@enum': function() {
if ( this._stdActions.isPlaceholder() ) {
this.emit('_resolveUp', this._stdActions);
}
return this._choices;
},
'@meta': {
'index': 1,
'row': 0,
'autocompletionUrl': '/project/mistral/actions/types'
'row': 0
}
})
},
'base-input': {
'@class': fields.directeddictionary.extend({}, {
'@class': fields.dictionary.extend({
create: function(json, parameters) {
var self = fields.dictionary.create.call(this, json, parameters);
self.setType('frozendict');
return self;
}
}, {
'@required': false,
'?': {
'@class': fields.string.extend({}, {
'@meta': {
'row': 0
}
})
},
'@meta': {
'index': 2,
'title': 'Base Input'
},
'?': {
'@class': fields.string.extend({}, {
'@meta': {'row': 1}
})
}
})
},
@ -409,11 +431,10 @@
});
return self;
},
_choices: null
_choices: []
}, {
'@enum': function() {
if (!this._choices) {
this._choices = [];
if ( this._actions.isPlaceholder() ) {
this.emit('_resolveUp', this._actions);
}
return this._choices;
@ -459,11 +480,10 @@
});
return self;
},
_choices: null
_choices: []
}, {
'@enum': function() {
if ( !this._choices ) {
this._choices = [];
if ( this._workflows.isPlaceholder() ) {
this.emit('_resolveUp', this._workflows);
}
return this._choices;
@ -548,7 +568,7 @@
params.id = taskId;
self.set(taskPos, TaskFactory(taskData, params));
} else if ( op === 'taskRemove' ) {
self.remove(arg);
self.removeItem(arg);
}
});
return self;
@ -702,6 +722,26 @@
}
});
models.StandardActions = Barricade.create({
'@type': Object,
'?': {
'@type': Array,
'*': {
'@type': String
}
}
});
models.Root = Barricade.ImmutableObject.extend({}, {
'@type': Object,
'standardActions': {
'@class': models.StandardActions
},
'workbook': {
'@class': models.Workbook
}
});
return models;
}])
})();

View File

@ -208,8 +208,8 @@
var regexp = new RegExp('(' + baseKey + ')([0-9]+)'),
newValue;
newID = newID || baseKey + utils.getNextIDSuffix(self, regexp);
if (_elClass.instanceof(Barricade.ImmutableObject)) {
if ('name' in _elClass._schema) {
if ( _elClass.instanceof(Barricade.ImmutableObject) ) {
if ( 'name' in _elClass._schema ) {
var nameNum = utils.getNextIDSuffix(self, regexp);
newValue = {name: baseName + nameNum};
} else {
@ -229,40 +229,33 @@
}
return _items;
};
self.empty = function() {
for ( var i = this._data.length; i > 0; i-- ) {
self.remove(i-1);
}
_items = [];
};
self.resetKeys = function(keys) {
self.empty();
keys.forEach(function(key) {
self.push(undefined, {id: key});
});
};
self._getContents = function() {
return self.toArray();
};
self.remove = function(key) {
self.removeItem = function(key) {
var pos = self.getPosByID(key);
Barricade.MutableObject.remove.call(self, pos);
self.remove(self.getPosByID(key));
_items.splice(pos, 1);
};
meldGroup.call(self);
// initialize cache with starting values
self.getValues();
return self;
}
}, {'@type': Object});
var directedDictionaryModel = dictionaryModel.extend({
create: function(json, parameters) {
var self = dictionaryModel.create.call(this, json, parameters);
self.setType('frozendict');
return self;
},
setSchema: function(keys) {
var self = this;
if ( keys !== undefined && keys !== null ) {
self.getIDs().forEach(function(oldKey) {
self.remove(oldKey);
});
keys.forEach(function(newKey) {
self.add(newKey);
});
}
}
}, {
'?': {'@type': String}
});
return {
string: stringModel,
text: textModel,
@ -270,7 +263,6 @@
list: listModel,
dictionary: dictionaryModel,
frozendict: frozendictModel,
directeddictionary: directedDictionaryModel,
autocompletionmixin: autoCompletionMixin,
wildcard: wildcardMixin // use for most general type-checks
};

View File

@ -52,8 +52,9 @@
}
},
remove: function() {
var container = this._barricadeContainer;
container.remove.call(container, this._barricadeId);
var container = this._barricadeContainer,
pos = container.getPosByID(this._barricadeId);
container.remove(pos);
}
};

View File

@ -10,7 +10,7 @@
ng-model="subvalue.value" ng-model-options="{ getterSetter: true }">
<span class="input-group-btn">
<button class="btn btn-default fa fa-minus-circle" type="button"
ng-click="value.remove(subvalue.keyValue())"></button>
ng-click="value.removeItem(subvalue.keyValue())"></button>
</span>
</div>
</div>

View File

@ -1,11 +1,11 @@
<collapsible-group content="value">
<div ng-repeat="row in value | extractRows track by row.id">
<div ng-class="{'three-columns': row.index !== undefined}">
<div ng-repeat="item in row | extractItems track by item.id"
<div ng-repeat="item in row | extractItems track by item.uid()"
ng-class="{'right-column': item.isAtomic() && $odd, 'left-column': item.isAtomic() && $even}">
<div class="form-group">
<label for="elem-{$ $id $}.{$ item.getID() $}">{$ item.title() $}</label>
<input type="text" class="form-control" id="elem-{$ $id $}.{$ item.getID() $}" ng-model="item.value"
<label for="elem-{$ $id $}.{$ item.uid() $}">{$ item.title() $}</label>
<input type="text" class="form-control" id="elem-{$ $id $}.{$ item.uid() $}" ng-model="item.value"
ng-model-options="{getterSetter: true}">
</div>
<div class="clearfix" ng-if="$odd"></div>

View File

@ -159,27 +159,46 @@ describe('merlin filters', function() {
});
it('are given a separate panel for each MutableObject entry', function() {
var panels;
topLevelObj.set('key2', {
'id1': {'name': 'String1'},
'id2': {'name': 'String2'}
});
var panels = extractPanels(topLevelObj);
panels = extractPanels(topLevelObj);
expect(panels.length).toBe(2);
});
it('have their title exposed via .title() which mirrors their id', function() {
describe('', function() {
var panels;
topLevelObj.set('key2', {'id1': {'name': 'some name'}});
panels = extractPanels(topLevelObj);
expect(panels[0].title()).toBe('id1');
});
it('are removable (thus are not permanent)', function() {
var panels;
topLevelObj.set('key2', {'id1': {'name': 'String1'}});
panels = extractPanels(topLevelObj);
beforeEach(function() {
topLevelObj.set('key2', {'id1': {'name': 'some name'}});
panels = extractPanels(topLevelObj);
});
it('have their title exposed via .title() which mirrors their id', function() {
expect(panels[0].title()).toBe('id1');
});
it("panel's title() acts also as a setter of the underlying object id", function() {
panels[0].title('id2');
expect(panels[0].title()).toBe('id2');
expect(topLevelObj.get('key2').getByID('id2')).toBeDefined();
});
it('are removable (thus are not permanent)', function() {
expect(panels[0].removable).toBe(true);
});
it('remove() function actually removes a panel', function() {
panels[0].remove();
panels = extractPanels(topLevelObj);
expect(panels.length).toBe(0);
});
expect(panels[0].removable).toBe(true);
});
});
@ -360,7 +379,7 @@ describe('merlin filters', function() {
immutableObj.set('key2', {'id_1': {key1: 'String_1'}});
panels1 = extractPanels(immutableObj);
immutableObj.get('key2').remove('id_1');
immutableObj.get('key2').removeItem('id_1');
immutableObj.set('key2', {'id_1': {key1: 'String_1'}});
panels2 = extractPanels(immutableObj);
@ -645,7 +664,7 @@ describe('merlin filters', function() {
rows1 = extractRows(mutableObj),
rows2;
mutableObj.remove('id1');
mutableObj.removeItem('id1');
mutableObj.push('string1', {id: 'id1'});
rows2 = extractRows(mutableObj);

View File

@ -0,0 +1,144 @@
/* Copyright (c) 2015 Mirantis, Inc.
Licensed 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.
*/
describe('merlin models:', function() {
'use strict';
var fields;
beforeEach(function() {
module('merlin');
inject(function($injector) {
fields = $injector.get('merlin.field.models');
})
});
describe('dictionary field', function() {
var dictObj;
beforeEach(function() {
dictObj = fields.dictionary.extend({}, {
'?': {
'@class': fields.string
}
}).create({'id1': 'string1', 'id2': 'string2'});
});
function getValueFromCache(id) {
var value = undefined;
dictObj.getValues().forEach(function(item) {
if ( item.getID() === id ) {
value = item;
}
});
return value;
}
function getCacheIDs() {
return dictObj.getValues().map(function(item) {
return item.getID();
});
}
describe('getValues() method', function() {
it('caching works from the very beginning', function() {
expect(getCacheIDs()).toEqual(['id1', 'id2']);
});
it('keyValue() getter/setter can be used from the start', function() {
var value = getValueFromCache('id1');
expect(value.keyValue()).toBe('id1');
value.keyValue('id3');
expect(value.keyValue()).toBe('id3');
expect(dictObj.getByID('id3')).toBeDefined();
});
});
describe('add() method', function() {
it('adds an empty value with given key', function() {
dictObj.add('id3');
expect(dictObj.getByID('id3').get()).toBe('');
expect(getCacheIDs()).toEqual(['id1', 'id2', 'id3']);
});
it('keyValue() getter/setter can be used for added values', function() {
var value;
dictObj.add('id3');
value = getValueFromCache('id3');
expect(value.keyValue()).toBe('id3');
value.keyValue('id4');
expect(value.keyValue()).toBe('id4');
expect(dictObj.getByID('id4')).toBeDefined();
});
it('updates name automatically if baseName and baseKey are provided', function() {
var nestedDictObj = fields.dictionary.extend({}, {
'?': {
'@class': fields.frozendict.extend({}, {
'name': {
'@class': fields.string
},
'@meta': {
'baseName': 'Action ',
'baseKey': 'action'
}
})
}
}).create({'action1': {'name': "Action 1"}});
nestedDictObj.add('action2');
expect(nestedDictObj.getByID('action2').get('name').get()).toEqual('Action 2');
})
});
describe('empty() method', function() {
it('removes all entries in model and in cache', function() {
dictObj.empty();
expect(dictObj.getIDs().length).toBe(0);
expect(dictObj.getValues().length).toBe(0);
})
});
describe('resetKeys() method', function() {
it('re-sets dictionary contents to given keys, cache included', function() {
dictObj.resetKeys(['key1', 'key2']);
expect(dictObj.getIDs()).toEqual(['key1', 'key2']);
expect(dictObj.getByID('key1').get()).toBe('');
expect(dictObj.getByID('key2').get()).toBe('');
expect(getCacheIDs()).toEqual(['key1', 'key2']);
})
});
describe('removeItem() method', function() {
it('removes dictionary entry by key from model and cache', function() {
dictObj.removeItem('id1');
expect(dictObj.getByID('id1')).toBeUndefined();
expect(getCacheIDs()).toEqual(['id2']);
})
});
});
});