Enable the Workflow structure change on changing its type

To enable this fix, a massive refactoring to panel/row/item - i.e.,
the 'view' mixins - was done. Now these mixins are implemented as
filters, which has the following benefits:
 * once the underlying Barricade model changes, the following change is
 immediately propagated to the view (input events are no longer
 catched by some obsolete piece of model just because view is not
 updated and is still sending events to an old model);
 * at the same time the filter results are memoized (with the
 appropriate function from underscore.js) so not infinite $digest
 looping happens, with the Barricade objects' string hash being used
 as a parts of a composite caching key.

Also change .toJSON() usage in Mistral models to .toJSON({pretty: true})
usage to not interfere with deserializing JSON blobs when we need to
recreate a Barricade object from JSON blob.

Provide a foundation of a suite of filters unit-tests - just a list of
specs being verified with an actual code to be written later.

Closes-Bug: #1436409
Change-Id: I7af36abbdcfab70a6b867c39e49420d1716c1310
This commit is contained in:
Timur Sufiev 2015-04-02 19:41:44 +03:00
parent d231eabe2b
commit 5014b8de58
13 changed files with 2148 additions and 275 deletions

View File

@ -34,14 +34,12 @@
var nextSuffix = getWorkbookNextIDSuffix(baseActionId),
newID = baseActionId + nextSuffix;
workbook.get('actions').push({name: 'Action ' + nextSuffix}, {id: newID});
workbook.addPanel(workbook.get('actions'), newID, workbook.get('actions').length());
};
$scope.addWorkflow = function() {
var nextSuffix = getWorkbookNextIDSuffix(baseWorkflowId),
newID = baseWorkflowId + nextSuffix;
workbook.get('workflows').push({name: 'Workflow ' + nextSuffix}, {id: newID});
workbook.addPanel(workbook.get('workflows'), newID);
};
}])

View File

@ -10,7 +10,7 @@
function(fields, panel, utils) {
var models = {};
var varlistValueFactory = function(json, parameters) {
function varlistValueFactory(json, parameters) {
var type = Barricade.getType(json);
if ( json === undefined || type === String ) {
return fields.string.create(json, parameters);
@ -23,7 +23,7 @@
'?': {'@class': fields.string}
}).create(json, parameters);
}
};
}
models.varlist = fields.list.extend({
create: function(json, parameters) {
@ -65,8 +65,8 @@
});
return self;
},
toJSON: function() {
var json = fields.frozendict.toJSON.apply(this, arguments);
_getPrettyJSON: function() {
var json = fields.frozendict._getPrettyJSON.apply(this, arguments);
return json.value;
}
}, {
@ -104,8 +104,8 @@
});
models.Action = fields.frozendict.extend({
toJSON: function() {
var json = fields.frozendict.toJSON.apply(this, arguments);
_getPrettyJSON: function() {
var json = fields.frozendict._getPrettyJSON.apply(this, arguments);
delete json.name;
return json;
}
@ -154,8 +154,8 @@
});
models.Task = fields.frozendict.extend({
toJSON: function() {
var json = fields.frozendict.toJSON.apply(this, arguments);
_getPrettyJSON: function() {
var json = fields.frozendict._getPrettyJSON.apply(this, arguments);
delete json.name;
return json;
}
@ -246,8 +246,8 @@
},
'policies': {
'@class': fields.frozendict.extend({
toJSON: function() {
var json = fields.frozendict.toJSON.apply(this, arguments);
_getPrettyJSON: function() {
var json = fields.frozendict._getPrettyJSON.apply(this, arguments);
json.retry = {
count: utils.pop(json, 'retry-count'),
delay: utils.pop(json, 'retry-delay'),
@ -324,8 +324,17 @@
});
models.Workflow = fields.frozendict.extend({
toJSON: function() {
var json = fields.frozendict.toJSON.apply(this, arguments);
create: function(json, parameters) {
var self = fields.frozendict.create.call(this, json, parameters);
self.on('childChange', function(child, op) {
if ( child === self.get('type') && op !== 'id' ) {
self.emit('change', 'workflowType');
}
});
return self;
},
_getPrettyJSON: function() {
var json = fields.frozendict._getPrettyJSON.apply(this, arguments);
delete json.name;
return json;
}
@ -370,6 +379,22 @@
}
})
},
'tasks': {
'@class': fields.dictionary.extend({}, {
'@meta': {
'index': 5,
'group': true
},
'?': {
'@class': models.Task
}
})
}
});
models.ReverseWorkflow = models.Workflow.extend({});
models.DirectWorkflow = models.Workflow.extend({}, {
'task-defaults': {
'@class': fields.frozendict.extend({}, {
'@required': false,
@ -403,28 +428,22 @@
})
}
})
},
'tasks': {
'@class': fields.dictionary.extend({}, {
'@meta': {
'index': 5,
'group': true
},
'?': {
'@class': models.Task
}
})
}
});
var workflowTypes = {
'direct': models.DirectWorkflow,
'reverse': models.ReverseWorkflow
};
function workflowFactory(json, parameters) {
var type = json.type || 'direct';
return workflowTypes[type].create(json, parameters);
}
models.Workbook = fields.frozendict.extend({
create: function(json, parameters) {
var self = fields.frozendict.create.call(this, json, parameters);
return panel.panelmixin.call(self);
},
toYAML: function() {
return jsyaml.dump(this.toJSON());
return jsyaml.dump(this.toJSON({pretty: true}));
}
}, {
'version': {
@ -470,13 +489,30 @@
})
},
'workflows': {
'@class': fields.dictionary.extend({}, {
'@class': fields.dictionary.extend({
create: function(json, parameters) {
var self = fields.dictionary.create.call(this, json, parameters);
self.on('childChange', function(child, op) {
if ( op === 'workflowType' ) {
var workflowId = child.getID(),
workflowPos = self.getPosByID(workflowId),
workflowData = child.toJSON(),
newType = child.get('type').get(),
newWorkflow = workflowTypes[newType].create(
workflowData, {id: workflowId});
self.set(workflowPos, newWorkflow);
}
});
return self;
}
}, {
'@meta': {
'index': 4,
'panelIndex': 2
},
'?': {
'@class': models.Workflow
'@class': models.Workflow,
'@factory': workflowFactory
}
})
}

View File

@ -13,8 +13,10 @@
{% include "horizon/_scripts.html" %}
<script type="text/javascript" src="{{ STATIC_URL }}merlin/js/lib/barricade.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}merlin/js/lib/js-yaml.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}merlin/js/lib/underscore.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}merlin/js/merlin.init.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}merlin/js/merlin.templates.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}merlin/js/merlin.filters.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}merlin/js/merlin.directives.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}merlin/js/merlin.field.models.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}merlin/js/merlin.panel.models.js"></script>
@ -60,12 +62,12 @@
<!-- Data panel start -->
<div class="two-panels">
<div class="left-panel">
<panel ng-repeat="panel in workbook.getPanels() track by panel.id"
<panel ng-repeat="panel in workbook | extractPanels track by panel.id"
title="{$ panel.getTitle() $}" removable="{$ panel.removable $}"
on-remove="panel.remove(panel.id)">
<div ng-repeat="row in panel.getRows() track by row.id">
on-remove="panel.remove()">
<div ng-repeat="row in panel | extractRows track by row.id">
<div ng-class="{'two-columns': row.index !== undefined }">
<div ng-repeat="item in row.getItems() track by item.id"
<div ng-repeat="item in row | extractItems track by item.id"
ng-class="{'right-column': item.isAtomic() && $odd, 'left-column': item.isAtomic() && $even}">
<typed-field title="{$ item.getTitle() $}" value="item" type="{$ item.getType() $}"></typed-field>
<div class="clearfix" ng-if="$odd"></div>

View File

@ -35,9 +35,11 @@ module.exports = function (config) {
files: [
'./bower_components/angular/angular.js',
'./bower_components/angular-mocks/angular-mocks.js',
'./merlin/static/merlin/js/lib/underscore-min.js',
'./merlin/static/merlin/js/merlin.init.js',
'./merlin/static/merlin/js/merlin.templates.js',
'./merlin/static/merlin/js/merlin.directives.js',
'./merlin/static/merlin/js/merlin.filters.js',
'./merlin/static/merlin/js/merlin.field.models.js',
'./merlin/static/merlin/js/merlin.panel.models.js',
'./merlin/static/merlin/js/merlin.utils.js',
@ -45,7 +47,8 @@ module.exports = function (config) {
'./merlin/static/merlin/js/lib/barricade.js',
'./merlin/static/merlin/js/lib/js-yaml.js',
'merlin/test/js/utilsSpec.js',
'merlin/test/js/templatesSpec.js'
'merlin/test/js/templatesSpec.js',
'merlin/test/js/filtersSpec.js'
],
exclude: [

View File

@ -47,7 +47,7 @@ var Barricade = (function () {
*/
create: function (f) {
return function g() {
if (!this.hasOwnProperty('_parents')) {
if (!Object.prototype.hasOwnProperty.call(this, '_parents')) {
Object.defineProperty(this, '_parents', {value: []});
}
this._parents.push(g);
@ -95,7 +95,7 @@ var Barricade = (function () {
function merge(target, source) {
forInKeys(source).forEach(function (key) {
if (target.hasOwnProperty(key) &&
if (Object.prototype.hasOwnProperty.call(target, key) &&
isPlainObject(target[key]) &&
isPlainObject(source[key])) {
merge(target[key], source[key]);
@ -141,7 +141,8 @@ var Barricade = (function () {
subject = this;
function hasMixin(obj, mixin) {
return obj.hasOwnProperty('_parents') &&
return Object.prototype.hasOwnProperty
.call(obj, '_parents') &&
obj._parents.some(function (_parent) {
return _instanceof.call(_parent, mixin);
});
@ -166,42 +167,58 @@ var Barricade = (function () {
* @mixin
* @memberof Barricade
*/
Identifiable = Blueprint.create(function (id) {
/**
* Returns the ID
* @method getID
* @memberof Barricade.Identifiable
* @instance
* @returns {String}
*/
this.getID = function () {
return id;
};
Identifiable = (function () {
var counter = 0;
return Blueprint.create(function (id) {
var uid = this._uidPrefix + counter++;
/**
* Checks whether the ID is set for this item.
* @method hasID
* @memberof Barricade.Identifiable
* @instance
* @returns {Boolean}
*/
this.hasID = function() {
return id !== undefined;
};
/**
* Returns the ID
* @method getID
* @memberof Barricade.Identifiable
* @instance
* @returns {String}
*/
this.getID = function () {
return id;
};
/**
* Sets the ID.
* @method setID
* @memberof Barricade.Identifiable
* @instance
* @param {String} newID
* @returns {self}
*/
this.setID = function (newID) {
id = newID;
return this.emit('change', 'id');
};
/**
* Gets the unique ID of this particular element
* @method uid
* @memberof Barricade.Identifiable
* @instance
* @returns {String}
*/
this.uid = function () {
return uid;
};
/**
* Checks whether the ID is set for this item.
* @method hasID
* @memberof Barricade.Identifiable
* @instance
* @returns {Boolean}
*/
this.hasID = function() {
return id !== undefined;
};
/**
* Sets the ID.
* @method setID
* @memberof Barricade.Identifiable
* @instance
* @param {String} newID
* @returns {self}
*/
this.setID = function (newID) {
id = newID;
return this.emit('change', 'id');
};
});
})();
/**
* Tracks whether an object is being "used" or not, which is a state that
@ -558,6 +575,12 @@ var Barricade = (function () {
return self;
},
/**
* @memberof Barricade.Base
* @private
*/
_uidPrefix: 'obj-',
/**
* @memberof Barricade.Base
* @private
@ -565,7 +588,7 @@ var Barricade = (function () {
_getDefaultValue: function () {
return this._schema.hasOwnProperty('@default')
? typeof this._schema['@default'] === 'function'
? this._schema['@default']()
? this._schema['@default'].call(this)
: this._schema['@default']
: this._schema['@type']();
},
@ -611,6 +634,14 @@ var Barricade = (function () {
throw new Error("sift() must be overridden in subclass");
},
/**
* @memberof Barricade.Base
* @private
*/
_getPrettyJSON: function (options) {
return this._getJSON(options);
},
/**
* Returns the primitive type of the Barricade object.
* @memberof Barricade.Base
@ -640,6 +671,25 @@ var Barricade = (function () {
*/
isRequired: function () {
return this._schema['@required'] !== false;
},
/**
* Returns the JSON representation of the Barricade object.
* @memberof Barricade.Base
* @instance
* @param {Object} [options]
An object containing options that affect the JSON result.
Current supported options are ignoreUnused (Boolean, defaults
to false), which skips keys with values that are unused in
objects, and pretty (Boolean, defaults to false), which gives
control to the method `_getPrettyJSON`.
* @returns {JSON}
*/
toJSON: function (options) {
options = options || {};
return options.pretty
? this._getPrettyJSON(options)
: this._getJSON(options);
}
}));
@ -828,6 +878,16 @@ var Barricade = (function () {
}, this);
},
/**
* @memberof Barricade.Arraylike
* @private
*/
_getJSON: function (options) {
return this._data.map(function (el) {
return el.toJSON(options);
});
},
/**
* @callback Barricade.Arraylike.eachCB
* @param {Number} index
@ -932,22 +992,6 @@ var Barricade = (function () {
*/
toArray: function () {
return this._data.slice(); // Shallow copy to prevent mutation
},
/**
* Converts the Arraylike and all of its elements to JSON.
* @memberof Barricade.Arraylike
* @instance
* @param {Boolean} [ignoreUnused]
Whether to include unused entries. Has no effect at Arraylike's
level, but is passed into each element for them to decide.
* @returns {Array} JSON array containing JSON representations of each
element.
*/
toJSON: function (ignoreUnused) {
return this._data.map(function (el) {
return el.toJSON(ignoreUnused);
});
}
});
@ -1018,6 +1062,20 @@ var Barricade = (function () {
}
},
/**
* @memberof Barricade.ImmutableObject
* @private
*/
_getJSON: function (options) {
var data = this._data;
return this.getKeys().reduce(function (jsonOut, key) {
if (options.ignoreUnused !== true || data[key].isUsed()) {
jsonOut[key] = data[key].toJSON(options);
}
return jsonOut;
}, {});
},
/**
* @callback Barricade.ImmutableObject.eachCB
* @param {String} key
@ -1079,25 +1137,6 @@ var Barricade = (function () {
*/
isEmpty: function () {
return !Object.keys(this._data).length;
},
/**
* @memberof Barricade.ImmutableObject
* @instance
* @param {Boolean} [ignoreUnused]
Whether to include unused entries. If true, unused values will
not have their key/value pairs show up in the resulting JSON.
* @returns {Object} JSON representation of the ImmutableObject and its
values.
*/
toJSON: function (ignoreUnused) {
var data = this._data;
return this.getKeys().reduce(function (jsonOut, key) {
if (ignoreUnused !== true || data[key].isUsed()) {
jsonOut[key] = data[key].toJSON(ignoreUnused);
}
return jsonOut;
}, {});
}
});
@ -1113,6 +1152,21 @@ var Barricade = (function () {
*/
_elSymbol: '?',
/**
* @memberof Barricade.MutableObject
* @private
*/
_getJSON: function (options) {
return this.toArray().reduce(function (jsonOut, element) {
if (jsonOut.hasOwnProperty(element.getID())) {
logError("ID found multiple times: " + element.getID());
} else {
jsonOut[element.getID()] = element.toJSON(options);
}
return jsonOut;
}, {});
},
/**
* @memberof Barricade.MutableObject
* @private
@ -1191,28 +1245,6 @@ var Barricade = (function () {
} else {
return Arraylike.push.call(this, newJson, newParameters);
}
},
/**
* Converts the MutableObject and all of its elements to JSON.
* @memberof Barricade.MutableObject
* @instance
* @param {Boolean} [ignoreUnused]
Whether to include unused entries. If true, elements that are
unused will not be included in the return value. This parameter
is also passed to each element's `toJSON()` method.
* @returns {Object} JSON object containing JSON representations of each
element.
*/
toJSON: function (ignoreUnused) {
return this.toArray().reduce(function (jsonOut, element) {
if (jsonOut.hasOwnProperty(element.getID())) {
logError("ID found multiple times: " + element.getID());
} else {
jsonOut[element.getID()] = element.toJSON(ignoreUnused);
}
return jsonOut;
}, {});
}
});
@ -1230,6 +1262,14 @@ var Barricade = (function () {
return json;
},
/**
* @memberof Barricade.Primitive
* @private
*/
_getJSON: function () {
return this._data;
},
/**
* Retrieves the Primitive's value.
* @memberof Barricade.Primitive
@ -1282,16 +1322,6 @@ var Barricade = (function () {
logError("Setter - new value (", newVal, ")",
" did not match schema: ", schema);
return this;
},
/**
* Converts the Primitive to JSON (which is simply the value itself).
* @memberof Barricade.Primitive
* @instance
* @returns {JSON}
*/
toJSON: function () {
return this._data;
}
});
@ -1315,8 +1345,8 @@ var Barricade = (function () {
}());
function logError() {
console.error.apply(console, Array.prototype.slice.call(arguments)
.unshift('Barricade: '));
var args = Array.prototype.slice.call(arguments);
console.error.apply(console, ['Barricade: '].concat(args));
}
BarricadeMain = {
@ -1327,9 +1357,11 @@ var Barricade = (function () {
'Container': Container,
'Deferrable': Deferrable,
'Enumerated': Enumerated,
'Extendable': Extendable,
'getType': getType, // Very helpful function
'Identifiable': Identifiable,
'ImmutableObject': ImmutableObject,
'InstanceofMixin': InstanceofMixin,
'MutableObject': MutableObject,
'Observable': Observable,
'Omittable': Omittable,

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -134,7 +134,6 @@
return self.get(key);
})
};
panels.rowmixin.call(self);
meldGroup.call(self);
return self;
}
@ -151,11 +150,12 @@
modelMixin.call(self, 'dictionary');
self.add = function() {
var newID = baseKey + utils.getNextIDSuffix(self, /(key)([0-9]+)/),
var regexp = new RegExp('(' + baseKey + ')([0-9]+)'),
newID = baseKey + utils.getNextIDSuffix(self, regexp),
newValue;
if ( _elClass.instanceof(Barricade.ImmutableObject) ) {
if ( 'name' in _elClass._schema ) {
var nameNum = utils.getNextIDSuffix(self, new RegExp('(' + baseName + ')([0-9]+)'));
var nameNum = utils.getNextIDSuffix(self, regexp);
newValue = {name: baseName + nameNum};
} else {
newValue = {};

View File

@ -0,0 +1,143 @@
/* 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.
*/
(function() {
angular.module('merlin')
.filter('extractPanels', ['merlin.utils', function(utils) {
var panelProto = {
create: function(itemsOrContainer, id) {
if ( angular.isArray(itemsOrContainer) && !itemsOrContainer.length ) {
return null;
}
this.id = utils.getNewId();
if ( angular.isArray(itemsOrContainer) ) {
this.items = itemsOrContainer;
} else {
this._barricadeContainer = itemsOrContainer;
this._barricadeId = id;
var barricadeObj = itemsOrContainer.getByID(id);
this.items = barricadeObj.getKeys().map(function(key) {
return utils.enhanceItemWithID(barricadeObj.get(key), key);
});
this.removable = true;
}
return this;
},
getTitle: function() {
if ( this._barricadeContainer ) {
return this._barricadeContainer.getByID(this._barricadeId).get('name');
}
},
remove: function() {
var container = this._barricadeContainer;
container.remove.call(container, this._barricadeId);
}
};
function isPanelsRoot(item) {
try {
// check for 'actions' and 'workflows' containers
return item.instanceof(Barricade.MutableObject);
}
catch(err) {
return false;
}
}
function extractPanelsRoot(items) {
return isPanelsRoot(items[0]) ? items[0] : null;
}
return _.memoize(function(container) {
var items = container._getContents(),
panels = [];
utils.groupByMetaKey(items, 'panelIndex').forEach(function(items) {
var panelsRoot = extractPanelsRoot(items);
if ( panelsRoot ) {
panelsRoot.getIDs().forEach(function(id) {
panels.push(Object.create(panelProto).create(panelsRoot, id));
});
} else {
panels.push(Object.create(panelProto).create(items));
}
});
return panels.condense();
}, function(container) {
var hash = '';
container.getKeys().map(function(key) {
var item = container.get(key);
if ( isPanelsRoot(item) ) {
item.getIDs().forEach(function(id) {
hash += item.getByID(id).uid();
});
}
});
return hash;
});
}])
.filter('extractRows', ['merlin.utils', function(utils) {
function getItems(panelOrContainer) {
if ( panelOrContainer.items ) {
return panelOrContainer.items;
} else if ( panelOrContainer.getKeys ) {
return panelOrContainer.getKeys().map(function(key) {
return panelOrContainer.get(key);
});
} else {
return panelOrContainer.getIDs().map(function(id) {
return panelOrContainer.getByID(id);
});
}
}
return _.memoize(function(panel) {
var rowProto = {
create: function(items) {
this.id = utils.getNewId();
this.index = items.row;
this.items = items.slice();
return this;
}
};
return utils.groupByMetaKey(getItems(panel), 'row').map(function(items) {
return Object.create(rowProto).create(items);
});
}, function(panel) {
var hash = '';
getItems(panel).forEach(function(item) {
hash += item.uid();
});
return hash;
})
}])
.filter('extractItems', ['merlin.utils', function(utils) {
return _.memoize(function(row) {
return row.items.sort(function(item1, item2) {
return utils.getMeta(item1, 'index') - utils.getMeta(item2, 'index');
});
}, function(row) {
var hash = '';
row.items.forEach(function(item) {
hash += item.uid();
});
return hash;
})
}])
})();

View File

@ -6,132 +6,11 @@
angular.module('merlin')
.factory('merlin.panel.models', ['merlin.utils', function(utils) {
var rowProto = {
create: function(items) {
this.id = utils.getNewId();
this.index = items.row;
items = items.slice();
this._items = items.sort(function(item1, item2) {
return utils.getMeta(item1, 'index') - utils.getMeta(item2, 'index');
});
return this;
},
getItems: function() {
return this._items;
}
};
var panelMixin = Barricade.Blueprint.create(function (schema) {
var self = this,
panelProto = {
create: function(itemsOrContainer, id) {
if ( angular.isArray(itemsOrContainer) && !itemsOrContainer.length ) {
return null;
}
this.id = utils.getNewId();
if ( angular.isArray(itemsOrContainer) ) {
this._items = itemsOrContainer;
} else {
this._barricadeContainer = itemsOrContainer;
this._barricadeId = id;
var barricadeObj = itemsOrContainer.getByID(id);
this._items = barricadeObj.getKeys().map(function(key) {
return utils.enhanceItemWithID(barricadeObj.get(key), key);
});
this.removable = true;
}
return this;
},
getTitle: function() {
if ( this._barricadeContainer ) {
return this._barricadeContainer.getByID(this._barricadeId).get('name');
}
},
getRows: function() {
if ( this._rows === undefined ) {
this._rows = utils.groupByMetaKey(this._items, 'row').map(function(items) {
return Object.create(rowProto).create(items);
});
}
return this._rows;
},
remove: function(id) {
for ( var i = 0; i < panels.length; i++ ) {
if ( panels[i].id === id ) {
var container = this._barricadeContainer;
container.remove.call(container, this._barricadeId);
panels.splice(i, 1);
break;
}
}
}
},
panels;
this.getPanels = function(filterKey) {
if ( panels === undefined ) {
panels = [];
var items = self._getContents();
utils.groupByMetaKey(items, 'panelIndex').forEach(function(items) {
// check for 'actions' and 'workflows' containers
if ( items[0].instanceof(Barricade.MutableObject) ) {
items[0].getIDs().forEach(function(id) {
panels.push(Object.create(panelProto).create(items[0], id));
});
} else {
panels.push(Object.create(panelProto).create(items));
}
});
panels = panels.condense();
}
if ( filterKey ) {
panels.filter(function(panel) {
return panel._barricadeId && panel._barricadeId.match(filterKey);
})
}
return panels;
};
this.addPanel = function(barricadeContainer, itemID, panelIndex) {
var panel = Object.create(panelProto).create(barricadeContainer, itemID);
if ( panelIndex ) {
panels.splice(panelIndex, 0, panel);
}else {
panels.push(panel);
}
};
return this;
});
var rowMixin = Barricade.Blueprint.create(function() {
var self = this,
items = self._getContents(),
rows;
self.getRows = function() {
if ( rows === undefined ) {
rows = utils.groupByMetaKey(items, 'row').map(function(items) {
return Object.create(rowProto).create(items);
});
}
return rows;
};
self.on('change', function(op) {
if ( op == 'add' ) {
var items = self._getContents();
utils.groupByMetaKey(items, 'row').forEach(function(items) {
rows.push(Object.create(rowProto).create(items));
})
} else if ( op == 'remove' ) {
}
});
});
var groupMixin = Barricade.Blueprint.create(function() {
var self = this,
additive = utils.getMeta(self, 'additive');
rowMixin.call(self);
if ( additive === undefined ) {
additive = true;
}
@ -144,9 +23,7 @@
});
return {
panelmixin: panelMixin,
groupmixin: groupMixin,
rowmixin: rowMixin
groupmixin: groupMixin
}
}])

View File

@ -1,7 +1,7 @@
<collapsible-group title="{$ title $}">
<div ng-repeat="row in value.getRows() track by row.id">
<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.getItems() track by item.id"
<div ng-repeat="item in row | extractItems track by item.id"
ng-class="{'right-column': item.isAtomic() && $odd, 'left-column': item.isAtomic() && $even}">
<div class="form-group">
<label for="elem-{$ $id $}.{$ item.getID() $}">{$ item.getTitle() $}</label>

View File

@ -1,8 +1,8 @@
<collapsible-group title="{$ title $}" additive="{$ value.isAdditive() $}"
on-add="value.add()">
<div ng-repeat="row in value.getRows() track by row.id">
<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.getItems() track by item.id"
<div ng-repeat="item in row | extractItems track by item.id"
ng-class="{'right-column': item.isAtomic() && $odd, 'left-column': item.isAtomic() && $even}">
<typed-field title="{$ item.getTitle() $}" value="item" type="{$ item.getType() $}"></typed-field>
<div class="clearfix" ng-if="$odd"></div>

View File

@ -0,0 +1,228 @@
/* 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 filters', function() {
'use strict';
var $filter, fields;
beforeEach(function() {
module('merlin');
inject(function($injector) {
$filter = $injector.get('$filter');
fields = $injector.get('merlin.field.models');
})
});
describe('extractPanels() behavior:', function() {
var extractPanels, simpleObjClass;
beforeEach(function() {
extractPanels = $filter('extractPanels');
simpleObjClass = fields.frozendict.extend({}, {
'@type': Object,
'key1': {
'@type': Number
},
'key2': {
'@type': String
}
});
});
describe('the filter relies upon `@meta` object with `panelIndex` key', function() {
it('and all fields without it are merged into a single panel', function() {
var simpleObj = simpleObjClass.create(),
panels = extractPanels(simpleObj);
expect(panels.length).toBe(1);
});
it('the filter is applied only to the top-level entries of the passed object', function() {
});
});
describe('panels generated from Barricade.MutableObject (non-permanent panels)', function() {
it('are given a separate panel for each MutableObject entry', function() {
});
it('have their title exposed via .getTitle() which mirrors `name` entry value', function() {
});
it('are removable (thus are not permanent)', function() {
});
it('could not be spliced into one entity by giving the same `panelIndex`', function() {
})
});
describe('panels generated from objects other than Barricade.MutableObject (permanent panels)', function() {
it('have fields marked with the same `panelIndex` in the one panel', function() {
});
it('number of panels is defined by number of different `panelIndex` keys', function() {
});
it('are ordered by the `panelIndex` ascension', function() {
});
it('have no title returned from .getTitle()', function() {
});
it('are not removable (thus are permanent)', function() {
})
});
describe('panels are cached,', function() {
it('and 2 consequent filter calls return the identical results', function() {
});
it("yet totally replacing the elements that go to permanent panels doesn't reset the cache", function() {
});
it('while totally replacing the top-level object of a non-permanent panel resets the cache', function() {
});
it("still totally replacing the object contained within top-level object of a non-permanent panel doesn't reset the cache", function() {
});
});
});
describe('extractRows() behavior:', function() {
var extractRows;
beforeEach(function() {
extractRows = $filter('extractRows');
});
describe('the filter is meant to be chainable', function() {
it('with extractPanels() results', function() {
});
it('with Barricade.ImmutableObject contents', function() {
});
it('even with Barricade.MutableObject contents', function() {
});
});
it("the filter is not meant to be chainable with Barricade " +
"objects other MutableObject or ImmutableObject", function() {
});
describe('the filter relies upon `@meta` object with `row` key', function() {
it('and all fields without it are given a separate row for each field', function() {
});
it('the filter is applied only to the top-level entries of the passed object', function() {
});
it('2 fields with the same `row` key are placed in the same row', function() {
});
it('rows are ordered by the `row` key ascension', function() {
});
});
describe('rows are cached,', function() {
it('and 2 consequent filter calls return the identical results', function() {
});
describe('but totally replacing one of the elements that are contained within', function() {
it("panel resets the cache", function() {
});
it("ImmutableObject resets the cache", function() {
});
it("MutableObject resets the cache", function() {
});
});
it("yet totally replacing the Object somewhere deeper doesn't reset the cache", function() {
});
});
});
describe('extractItems() behavior:', function() {
var extractItems;
beforeEach(function() {
extractItems = $filter('extractItems');
});
it('the filter is meant to be chainable only with extractRows() results', function() {
});
describe('the filter relies upon `@meta` object with `index` key', function() {
it('and all fields without it are processed w/o errors, but with unpredictable ordering', function() {
});
describe('fields are ordered by the `index` key ascension, this applies', function() {
it('to the fields with `row` key defined (ordering within a row)', function() {
});
it('to the fields w/o `row` key defined (ordering of anonymous rows)', function() {
});
});
});
});
});