diff --git a/bower.json b/bower.json index f9e9a07..11088b5 100644 --- a/bower.json +++ b/bower.json @@ -14,7 +14,8 @@ "angular-moment": "0.9.0", "angular-cache": "3.2.5", "js-yaml": "3.2.7", - "underscore": "1.8.3" + "underscore": "1.8.3", + "flexboxgrid": "6.2.0" }, "devDependencies": { "angular-mocks": "1.3.10", diff --git a/extensions/mistral/static/mistral/js/mistral.init.js b/extensions/mistral/static/mistral/js/mistral.init.js index 3e06ddf..5e1a015 100644 --- a/extensions/mistral/static/mistral/js/mistral.init.js +++ b/extensions/mistral/static/mistral/js/mistral.init.js @@ -12,7 +12,7 @@ function initModule(templates) { templates.prefetch('/static/mistral/templates/fields/', - ['varlist', 'yaqllist']); + ['yaqlfield']); } })(); diff --git a/extensions/mistral/static/mistral/js/mistral.workbook.controllers.js b/extensions/mistral/static/mistral/js/mistral.workbook.controllers.js index f4830dd..7416ae2 100644 --- a/extensions/mistral/static/mistral/js/mistral.workbook.controllers.js +++ b/extensions/mistral/static/mistral/js/mistral.workbook.controllers.js @@ -35,6 +35,20 @@ }); }; + // Please see the explanation of how this determinant function works + // in the 'extractPanels' filter documentation + vm.keyExtractor = function(item, parent) { + if (item.instanceof(models.Action)) { + return 500 + parent.toArray().indexOf(item); + } else if (item.instanceof(models.Workflow)) { + return 1000 + parent.toArray().indexOf(item); + } else if (item.instanceof(Barricade.Container)) { + return null; + } else { + return 0; + } + }; + function getNextIDSuffix(container, regexp) { var max = Math.max.apply(Math, container.getIDs().map(function(id) { var match = regexp.exec(id); diff --git a/extensions/mistral/static/mistral/js/mistral.workbook.models.js b/extensions/mistral/static/mistral/js/mistral.workbook.models.js index cad9f40..5f403eb 100644 --- a/extensions/mistral/static/mistral/js/mistral.workbook.models.js +++ b/extensions/mistral/static/mistral/js/mistral.workbook.models.js @@ -18,11 +18,15 @@ if ( angular.isUndefined(json) || type === String ) { return fields.string.create(json, parameters); } else if ( type === Array ) { - return fields.list.extend({}, { + return fields.list.extend({ + inline: true + }, { '*': {'@class': fields.string} }).create(json, parameters); } else if ( type === Object ) { - return fields.dictionary.extend({}, { + return fields.dictionary.extend({ + inline: true + }, { '?': {'@class': fields.string} }).create(json, parameters); } @@ -31,7 +35,6 @@ models.varlist = fields.list.extend({ create: function(json, parameters) { var self = fields.list.create.call(this, json, parameters); - self.setType('varlist'); self.on('childChange', function(child, op) { if ( op == 'empty' ) { self.each(function(index, item) { @@ -48,6 +51,7 @@ '@class': fields.frozendict.extend({ create: function(json, parameters) { var self = fields.frozendict.create.call(this, json, parameters); + self.isAtomic = function() { return false; }; self.on('childChange', function(child) { if ( child.instanceof(Barricade.Enumerated) ) { // type change var value = self.get('value'); @@ -87,25 +91,25 @@ } }); - models.yaqllist = fields.list.extend({ + models.YAQLField = fields.frozendict.extend({ create: function(json, parameters) { - var self = fields.list.create.call(this, json, parameters); - self.setType('yaqllist'); + var self = fields.frozendict.create.call(this, json, parameters); + self.setType('yaqlfield'); return self; } }, { - '*': { - '@class': fields.frozendict.extend({}, { - 'yaql': { - '@class': fields.string - }, - 'action': { - '@class': fields.string - } - }) + 'yaql': { + '@class': fields.string + }, + 'action': { + '@class': fields.string } }); + models.yaqllist = fields.list.extend({}, { + '*': {'@class': models.YAQLField} + }); + models.Action = fields.frozendict.extend({ create: function(json, parameters) { var self = fields.frozendict.create.call(this, json, parameters); @@ -135,8 +139,7 @@ } }, { '@meta': { - 'index': 1, - 'row': 0 + 'index': 1 } }) }, @@ -144,18 +147,24 @@ '@class': fields.dictionary.extend({ create: function(json, parameters) { var self = fields.dictionary.create.call(this, json, parameters); + self.isAdditive = function() { return false; }; self.setType('frozendict'); return self; + }, + // here we override `each' method inherited from fields.dictionary<-MutableObject + // because it provides entry index as the first argument of the callback, while + // we need to get the key/ID value as first argument (mimicking the `each' method + // ImmutableObject) + each: function(callback) { + var self = this; + this.getIDs().forEach(function(id) { + callback.call(self, id, self.getByID(id)); + }); + return this; } }, { '@required': false, - '?': { - '@class': fields.string.extend({}, { - '@meta': { - 'row': 0 - } - }) - }, + '?': {'@class': fields.string}, '@meta': { 'index': 2, 'title': 'Base Input' @@ -189,9 +198,6 @@ }); return self; }, - remove: function() { - this.emit('change', 'taskRemove', this.getID()); - }, _getPrettyJSON: function() { var json = fields.frozendict._getPrettyJSON.apply(this, arguments); delete json.type; @@ -200,10 +206,7 @@ }, { '@meta': { 'baseKey': 'task', - 'baseName': 'Task ', - 'group': true, - 'additive': false, - 'removable': true + 'baseName': 'Task ' }, 'type': { '@class': fields.string.extend({}, { @@ -214,16 +217,14 @@ }], '@default': 'action', '@meta': { - 'index': 0, - 'row': 0 + 'index': 0 } }) }, 'description': { '@class': fields.text.extend({}, { '@meta': { - 'index': 2, - 'row': 1 + 'index': 2 } }) }, @@ -268,7 +269,6 @@ '@required': false, '@meta': { 'index': 0, - 'row': 0, 'title': 'Wait before' } }) @@ -278,7 +278,6 @@ '@required': false, '@meta': { 'index': 1, - 'row': 0, 'title': 'Wait after' } }) @@ -287,8 +286,7 @@ '@class': fields.number.extend({}, { '@required': false, '@meta': { - 'index': 2, - 'row': 1 + 'index': 2 } }) }, @@ -297,7 +295,6 @@ '@required': false, '@meta': { 'index': 3, - 'row': 2, 'title': 'Retry count' } }) @@ -307,7 +304,6 @@ '@required': false, '@meta': { 'index': 4, - 'row': 2, 'title': 'Retry delay' } }) @@ -317,7 +313,6 @@ '@required': false, '@meta': { 'index': 5, - 'row': 3, 'title': 'Retry break on' } }) @@ -330,7 +325,6 @@ 'requires': { '@class': fields.string.extend({}, { '@meta': { - 'row': 2, 'index': 3 } }) @@ -386,7 +380,6 @@ } }, { '@meta': { - 'row': 0, 'index': 1 } }) @@ -407,7 +400,6 @@ } }, { '@meta': { - 'row': 0, 'index': 1 } }) @@ -446,8 +438,7 @@ '@enum': ['reverse', 'direct'], '@default': 'direct', '@meta': { - 'index': 1, - 'row': 0 + 'index': 1 } }) }, @@ -485,16 +476,13 @@ var taskData = child.toJSON(); params.id = taskId; self.set(taskPos, TaskFactory(taskData, params)); - } else if ( op === 'taskRemove' ) { - self.removeItem(arg); } }); return self; } }, { '@meta': { - 'index': 5, - 'group': true + 'index': 5 }, '?': { '@class': models.Task, @@ -511,9 +499,7 @@ '@class': fields.frozendict.extend({}, { '@required': false, '@meta': { - 'index': 4, - 'group': true, - 'additive': false + 'index': 4 }, 'on-error': { '@class': models.yaqllist.extend({}, { @@ -557,8 +543,7 @@ models.Actions = fields.dictionary.extend({}, { '@required': false, '@meta': { - 'index': 3, - 'panelIndex': 1 + 'index': 3 }, '?': { '@class': models.Action @@ -583,8 +568,7 @@ } }, { '@meta': { - 'index': 4, - 'panelIndex': 2 + 'index': 4 }, '?': { '@class': models.Workflow, @@ -601,9 +585,7 @@ '@class': fields.string.extend({}, { '@enum': ['2.0'], '@meta': { - 'index': 2, - 'panelIndex': 0, - 'row': 1 + 'index': 2 }, '@default': '2.0' }) @@ -611,9 +593,7 @@ 'name': { '@class': fields.string.extend({}, { '@meta': { - 'index': 0, - 'panelIndex': 0, - 'row': 0 + 'index': 0 }, '@constraints': [ function(value) { @@ -625,9 +605,7 @@ 'description': { '@class': fields.text.extend({}, { '@meta': { - 'index': 1, - 'panelIndex': 0, - 'row': 0 + 'index': 1 }, '@required': false }) diff --git a/extensions/mistral/static/mistral/templates/fields/varlist.html b/extensions/mistral/static/mistral/templates/fields/varlist.html deleted file mode 100644 index 0c3d3eb..0000000 --- a/extensions/mistral/static/mistral/templates/fields/varlist.html +++ /dev/null @@ -1,91 +0,0 @@ - -
-
-
- - -
-
-
- -
-
- -
- - - - -
-
-
- - -
-
-
-
-
- -
- - - - -
-
-
-
-
- -
-
-
- - -
-
-
-
-
- -
- - - - -
-
-
-
-
- -
-
-
- -
-
-
diff --git a/extensions/mistral/static/mistral/templates/fields/yaqlfield.html b/extensions/mistral/static/mistral/templates/fields/yaqlfield.html new file mode 100644 index 0000000..ddf2523 --- /dev/null +++ b/extensions/mistral/static/mistral/templates/fields/yaqlfield.html @@ -0,0 +1,22 @@ +
+
+
+ +
+
+
+
+
+ + + + +
+
+
+
diff --git a/extensions/mistral/static/mistral/templates/fields/yaqllist.html b/extensions/mistral/static/mistral/templates/fields/yaqllist.html deleted file mode 100644 index c314ea3..0000000 --- a/extensions/mistral/static/mistral/templates/fields/yaqllist.html +++ /dev/null @@ -1,30 +0,0 @@ - -
-
-
- -
-
-
-
-
- - - - - - - -
-
-
-
-
diff --git a/extensions/mistral/templates/mistral/create.html b/extensions/mistral/templates/mistral/create.html index 7033859..4ee0011 100644 --- a/extensions/mistral/templates/mistral/create.html +++ b/extensions/mistral/templates/mistral/create.html @@ -33,21 +33,22 @@ {% compress css %} {% endcompress %} + {% block merlin-css %}{% endblock %} {% endblock %} {% block main %}

Create Workbook

-
-
-
-
+
+
+

{$ wb.workbook.get('name') $}

-
-
+
+
-
-
+
+
-
-
- +
+ -
-
-
- -
+
+
+
+ + + +
+
+ +
+ +
+
-
+
{$ wb.workbook.toYAML() $}
@@ -93,14 +102,12 @@
-
-
-
- - -
+
+
+ +
diff --git a/extensions/mistral/test/js/workbook.model.spec.js b/extensions/mistral/test/js/workbook.model.spec.js index 5e80297..7a86f68 100644 --- a/extensions/mistral/test/js/workbook.model.spec.js +++ b/extensions/mistral/test/js/workbook.model.spec.js @@ -129,13 +129,6 @@ describe('workbook model logic', function() { expect(json.workflows[workflowID].tasks[newID]).toBeDefined(); }); - it('a task deletion works in conjunction with tasks logic', function() { - expect(getTask(taskID)).toBeDefined(); - - getTask(taskID).remove(); - expect(getTask(taskID)).toBeUndefined(); - }); - }); describe("which start with the 'direct' workflow:", function() { diff --git a/merlin/static/merlin/js/merlin.directives.js b/merlin/static/merlin/js/merlin.directives.js index 510e707..ace9c56 100644 --- a/merlin/static/merlin/js/merlin.directives.js +++ b/merlin/static/merlin/js/merlin.directives.js @@ -39,9 +39,21 @@ * retrieves a template by its name which is the same as model's type and renders it, * recursive -s are possible. * */ - .directive('typedField', typedField); + .directive('typedField', typedField) - typedField.$inject = ['$compile', 'merlin.templates']; + .directive('labeled', labeled); + + function labeled() { + return { + restrict: 'E', + templateUrl: '/static/merlin/templates/labeled.html', + transclude: true, + scope: { + label: '@', + for: '@' + } + }; + } function editable() { return { @@ -100,6 +112,7 @@ }; } + showFocus.$inject = ['$timeout']; function showFocus($timeout) { return function(scope, element, attrs) { // Unused variable created here due to rule 'ng_on_watch': 2 @@ -114,7 +127,7 @@ }; } - function panel($parse) { + function panel() { return { restrict: 'E', templateUrl: '/static/merlin/templates/collapsible-panel.html', @@ -122,9 +135,13 @@ scope: { panel: '=content' }, - link: function(scope, element, attrs) { - scope.removable = $parse(attrs.removable)(); - scope.isCollapsed = false; + link: function(scope) { + if (angular.isDefined(scope.panel)) { + scope.isCollapsed = false; + if (angular.isFunction(scope.panel.title)) { + scope.editable = true; + } + } } }; } @@ -136,11 +153,15 @@ transclude: true, scope: { group: '=content', + title: '=', onAdd: '&', onRemove: '&' }, link: function(scope, element, attrs) { scope.isCollapsed = false; + if (angular.isFunction(scope.title)) { + scope.editable = true; + } if ( attrs.onAdd && attrs.additive !== 'false' ) { scope.additive = true; } @@ -151,6 +172,7 @@ }; } + validatableWith.$inject = ['$parse']; function validatableWith($parse) { return { restrict: 'A', @@ -186,6 +208,7 @@ }; } + typedField.$inject = ['$compile', 'merlin.templates']; function typedField($compile, templates) { return { restrict: 'E', @@ -195,7 +218,7 @@ }, link: function(scope, element) { templates.templateReady(scope.type).then(function(template) { - element.replaceWith($compile(template)(scope)); + element.append($compile(template)(scope)); }); } }; diff --git a/merlin/static/merlin/js/merlin.field.models.js b/merlin/static/merlin/js/merlin.field.models.js index c068e3b..2d1cf08 100644 --- a/merlin/static/merlin/js/merlin.field.models.js +++ b/merlin/static/merlin/js/merlin.field.models.js @@ -61,6 +61,24 @@ return this; }); + /* Html renderer helper. The main idea is that fields with simple (or plain) + structure (i.e. Atomics = string | number | text | boolean and list or + dictionary containing just Atomics) could be rendered in one column, while + fields with non plain structure should be rendered in two columns. + */ + var plainStructureMixin = Barricade.Blueprint.create(function() { + this.isPlainStructure = function() { + if (this.getType() == 'frozendict') { + return false; + } + if (!this.instanceof(Barricade.Arraylike) || !this.length()) { + return false; + } + return !this.get(0).instanceof(Barricade.Container); + }; + return this; + }); + var modelMixin = Barricade.Blueprint.create(function(type) { var isValid = true; var isValidatable = false; @@ -90,8 +108,12 @@ type = _type; }; + this.isAdditive = function() { + return this.instanceof(Barricade.Arraylike); + }; + this.isAtomic = function() { - return ['number', 'string', 'text', 'choices'].indexOf(this.getType()) > -1; + return !this.instanceof(Barricade.Container); }; this.title = function() { var title = utils.getMeta(this, 'title'); @@ -148,13 +170,8 @@ self.add = function() { self.push(undefined, parameters); }; - self.getValues = function() { - return self.toArray(); - }; - self._getContents = function() { - return self.toArray(); - }; meldGroup.call(self); + plainStructureMixin.call(self); return self; } }, {'@type': Array}); @@ -162,20 +179,10 @@ var frozendictModel = Barricade.ImmutableObject.extend({ create: function(json, parameters) { var self = Barricade.ImmutableObject.create.call(this, json, parameters); - self.getKeys().forEach(function(key) { - utils.enhanceItemWithID(self.get(key), key); - }); modelMixin.call(self, 'frozendict'); - self.getValues = function() { - return self._data; - }; - self._getContents = function() { - return self.getKeys().map(function(key) { - return self.get(key); - }); - }; meldGroup.call(self); + plainStructureMixin.call(self); return self; } }, {'@type': Object}); @@ -183,15 +190,14 @@ var dictionaryModel = Barricade.MutableObject.extend({ create: function(json, parameters) { var self = Barricade.MutableObject.create.call(this, json, parameters); - var _items = []; var _elClass = self._elementClass; var baseKey = utils.getMeta(_elClass, 'baseKey') || 'key'; var baseName = utils.getMeta(_elClass, 'baseName') || utils.makeTitle(baseKey); modelMixin.call(self, 'dictionary'); + plainStructureMixin.call(self); - function makeCacheWrapper(container, key) { - var value = container.getByID(key); + function initKeyAccessor(value) { value.keyValue = function () { if ( arguments.length ) { value.setID(arguments[0]); @@ -199,9 +205,16 @@ return value.getID(); } }; - return value; } + self.each(function(key, value) { + initKeyAccessor(value); + }).on('change', function(op, index) { + if (op === 'add' || op === 'set') { + initKeyAccessor(self.get(index)); + } + }); + self.add = function(newID) { var regexp = new RegExp('(' + baseKey + ')([0-9]+)'); var newValue; @@ -217,21 +230,11 @@ newValue = ''; } self.push(newValue, utils.extend(self._parameters, {id: newID})); - _items.push(makeCacheWrapper(self, newID)); - }; - self.getValues = function() { - if ( !_items.length ) { - _items = self.toArray().map(function(value) { - return makeCacheWrapper(self, value.getID()); - }); - } - 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(); @@ -239,17 +242,10 @@ self.push(undefined, {id: key}); }); }; - self._getContents = function() { - return self.toArray(); - }; self.removeItem = function(key) { - var pos = self.getPosByID(key); self.remove(self.getPosByID(key)); - _items.splice(pos, 1); }; meldGroup.call(self); - // initialize cache with starting values - self.getValues(); return self; } }, {'@type': Object}); diff --git a/merlin/static/merlin/js/merlin.filters.js b/merlin/static/merlin/js/merlin.filters.js index ba5f6fa..6334eaf 100644 --- a/merlin/static/merlin/js/merlin.filters.js +++ b/merlin/static/merlin/js/merlin.filters.js @@ -16,148 +16,187 @@ (function() { angular .module('merlin') + /* 'extractPanels' filter requires one argument which should be a function. + This function is applied to the top-level elements of the object and the + fields for which it returns a numeric value are grouped into the panels. More + precisely, each field yielding the same numeric value is put into the same panel. + Subclasses of Barricade.Container which don't yield a numeric value (and return + null, for example) become the entry points of a recursive application of above + algorithm, so eventually each field will be either: + * put into a panel (determinant returns numeric value) + * recursively scanned for more fields (is a container, no numeric value returned) + * or skipped completely (neither of above conditions is met). + + Each returned panel implements at least .each() method (iterating through all key & + field pairs of a panel) which could be later consumed by 'extractFields' filter. + Filter results are cached, with each field explicitly put into a panel by determinant + (i.e. yielding a numeric value) adds its unique id to the caching key. This means that + the filter returns a new set of panels if the set of fields explicitly put into panels + changes - i.e. a value goes away or comes in into a set or replaced in place with + another value (any case is tracked by the unique field id). + */ .filter('extractPanels', extractPanels) - .filter('extractRows', extractRows) - .filter('extractItems', extractItems); + .filter('extractFields', extractFields) + .filter('chunks', chunks); extractPanels.$inject = ['merlin.utils']; - extractRows.$inject = ['merlin.utils']; - extractItems.$inject = ['merlin.utils']; function extractPanels(utils) { var panelProto = { - create: function(itemsOrContainer, id) { - if ( angular.isArray(itemsOrContainer) && !itemsOrContainer.length ) { - return null; - } - if ( angular.isArray(itemsOrContainer) ) { - this.items = itemsOrContainer; - this.id = itemsOrContainer.reduce(function(prevId, item) { - return item.uid() + prevId; - }, ''); + create: function(enumerator, obj, context) { + this.$$obj = obj; + this.$$enumerator = enumerator; + this.removable = false; + if (this.$$obj) { + this.id = this.$$obj.uid(); + this.$$objParent = context.container; + this.removable = this.$$objParent.instanceof(Barricade.Arraylike); + if (this.$$objParent.instanceof(Barricade.MutableObject)) { + this.title = function() { + if ( arguments.length ) { + obj.setID(arguments[0]); + } else { + return obj.getID(); + } + }; + } else if (this.$$objParent.instanceof(Barricade.ImmutableObject)) { + this.title = context.indexOrKey; + } } else { - this._barricadeContainer = itemsOrContainer; - this._barricadeId = id; - var barricadeObj = itemsOrContainer.getByID(id); - this.id = barricadeObj.uid(); - this.items = barricadeObj.getKeys().map(function(key) { - return utils.enhanceItemWithID(barricadeObj.get(key), key); + var id = ''; + this.$$enumerator(function(key, item) { + id += item.uid(); }); - this.removable = true; + this.id = id; } return this; }, - title: function() { - var newID; - if ( this._barricadeContainer ) { - if ( arguments.length ) { - newID = arguments[0]; - this._barricadeContainer.getByID(this._barricadeId).setID(newID); - this._barricadeId = newID; - } else { - return this._barricadeId; - } - } + each: function(callback, comparator) { + this.$$enumerator.call(this.$$obj, callback, comparator); }, remove: function() { - var container = this._barricadeContainer; - var pos = container.getPosByID(this._barricadeId); - container.remove(pos); + var index; + if (this.removable) { + index = this.$$objParent.toArray().indexOf(this.$$obj); + this.$$objParent.remove(index); + } } }; - 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(); + return _.memoize(function(container, keyExtractor) { + var items = []; + var _data = {}; var 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)); - }); + + /* This function recursively applies determinant 'keyExtractor' function + to each container (given that the determinant doesn't return a numeric + value for it), starting from the top-level. Fields for which determinant + returns a numeric value, will be later placed into a panels (see docs for + 'extractPanels' filter). + */ + function rec(container) { + container.each(function(indexOrKey, item) { + var groupingKey = keyExtractor(item, container); + if (angular.isNumber(groupingKey)) { + items.push(item); + _data[item.uid()] = { + groupingKey: groupingKey, + container: container, + indexOrKey: indexOrKey + }; + } else if (item.instanceof(Barricade.Container)) { + rec(item); + } + }); + } + // top-level entry-point of recursive descent + rec(container); + + function extractKey(item) { + return angular.isDefined(item) && _data[item.uid()].groupingKey; + } + + utils.groupByExtractedKey(items, extractKey).forEach(function(items) { + var parent, enumerator, obj, context; + if (items.length > 1 || !items[0].instanceof(Barricade.Container)) { + parent = _data[items[0].uid()].container; + // the enumerator function mimicking the behavior of built-in .each() + // method which aggregate panels do not have + enumerator = function(callback) { + items.forEach(function(item) { + if (_data[item.uid()].container === parent) { + callback(_data[item.uid()].indexOrKey, item); + } + }); + }; } else { - panels.push(Object.create(panelProto).create(items)); + obj = items[0]; + enumerator = obj.each; + context = _data[obj.uid()]; } + panels.push(Object.create(panelProto).create(enumerator, obj, context)); }); return utils.condense(panels); - }, function(container) { + }, function(container, keyExtractor) { 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(); - }); - } else { - hash += item.uid(); - } - }); - return hash; - }); - } - - function extractRows(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); + function rec(container) { + container.each(function(indexOrKey, item) { + var groupingKey = keyExtractor(item, container); + if (angular.isNumber(groupingKey)) { + hash += item.uid(); + } else if (item.instanceof(Barricade.Container)) { + rec(item); + } }); } - } + rec(container); + return hash; + }); + } - return _.memoize(function(panel) { - var rowProto = { - create: function(items) { - this.id = items[0].uid(); - 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 extractFields() { + return _.memoize(function(container) { + var fields = {}; + container.each(function(key, item) { + fields[key] = item; }); + return fields; }, function(panel) { var hash = ''; - getItems(panel).forEach(function(item) { + panel.each(function(key, item) { hash += item.uid(); }); return hash; }); } - function extractItems(utils) { - return _.memoize(function(row) { - return row.items.sort(function(item1, item2) { - return utils.getMeta(item1, 'index') - utils.getMeta(item2, 'index'); - }); - }, function(row) { + function chunks() { + return _.memoize(function(fields, itemsPerChunk) { + var chunks = []; + var keys = Object.keys(fields); + var i, j, chunk; + itemsPerChunk = +itemsPerChunk; + if (!angular.isNumber(itemsPerChunk) || itemsPerChunk < 1) { + return chunks; + } + for (i = 0; i < keys.length; i++) { + chunk = {}; + for (j = 0; j < itemsPerChunk; j++) { + chunk[keys[i]] = fields[keys[i]]; + } + chunks.push(chunk); + } + return chunks; + }, function(fields) { var hash = ''; - row.items.forEach(function(item) { - hash += item.uid(); - }); + var key; + for (key in fields) { + if (fields.hasOwnProperty(key)) { + hash += fields[key].uid(); + } + } return hash; }); } + })(); diff --git a/merlin/static/merlin/js/merlin.init.js b/merlin/static/merlin/js/merlin.init.js index 4a51a5d..5a53dcd 100644 --- a/merlin/static/merlin/js/merlin.init.js +++ b/merlin/static/merlin/js/merlin.init.js @@ -19,7 +19,7 @@ function fieldTemplates() { return [ 'dictionary', 'frozendict', 'list', - 'string', 'text', 'group', 'number', 'choices' + 'string', 'text', 'number', 'choices' ]; } diff --git a/merlin/static/merlin/js/merlin.utils.js b/merlin/static/merlin/js/merlin.utils.js index 8bad961..51089a6 100644 --- a/merlin/static/merlin/js/merlin.utils.js +++ b/merlin/static/merlin/js/merlin.utils.js @@ -23,16 +23,16 @@ return 'id-' + idCounter; } - function groupByMetaKey(sequence, metaKey, insertAtBeginning) { + function groupByExtractedKey(sequence, keyExtractor, insertAtBeginning) { var newSequence = []; var defaultBucket = []; var index; sequence.forEach(function(item) { - index = getMeta(item, metaKey); + index = keyExtractor(item); if ( angular.isDefined(index) ) { if ( !newSequence[index] ) { newSequence[index] = []; - newSequence[index][metaKey] = index; + newSequence[index][keyExtractor()] = index; } newSequence[index].push(item); } else { @@ -51,6 +51,17 @@ return newSequence; } + function groupByMetaKey(sequence, metaKey, insertAtBeginning) { + function keyExtractor(item) { + if (angular.isDefined(item)) { + return getMeta(item, metaKey); + } else { + return metaKey; + } + } + return groupByExtractedKey(sequence, keyExtractor, insertAtBeginning); + } + function getMeta(item, key) { if ( item ) { var meta = item._schema['@meta']; @@ -103,6 +114,7 @@ getMeta: getMeta, getNewId: getNewId, groupByMetaKey: groupByMetaKey, + groupByExtractedKey: groupByExtractedKey, makeTitle: makeTitle, getNextIDSuffix: getNextIDSuffix, enhanceItemWithID: enhanceItemWithID, diff --git a/merlin/static/merlin/scss/merlin.scss b/merlin/static/merlin/scss/merlin.scss index b33a08c..fa84b67 100644 --- a/merlin/static/merlin/scss/merlin.scss +++ b/merlin/static/merlin/scss/merlin.scss @@ -1,53 +1,8 @@ -@import "/bootstrap/scss/bootstrap"; - -.two-panels { - @include make-row(); - .left-panel { - @include make-xs-column(6); - } - .right-panel { - @include make-xs-column(6); - } - .full-width { - @include make-xs-column(12); - } -} - -.two-columns { - @include make-row(); - .left-column { - @include make-xs-column(6); - } - .right-column { - @include make-xs-column(6); - } -} - -.three-columns { - @include make-row(); - .left-column { - @include make-xs-column(5); - } - .right-column { - @include make-xs-column(5); - } - .both-columns { - @include make-xs-column(10); - } - .button-column { - @include make-xs-column(2); - } -} - .panel-default.merlin-panel { .panel-heading { color: inherit; background-color: inherit; border: none; - padding-left: 20px; - } - .panel-body { - padding-left: 20px; } textarea { resize: vertical; @@ -64,20 +19,16 @@ editable { } .section { - .form-group { - padding-left: 15px; - } - .section { - margin-left: 15px; - } - a { - padding-left: 5px; - text-decoration: none; - color: black; - } h5 { font-weight: bold; } + + .section-heading { + a { + text-decoration: none; + color: black; + } + } } .fa-minus-circle { @@ -93,28 +44,8 @@ editable { } } -.popover-content > button { - margin: 5px; - float: right; -} - -.popover.right { - width: 200px; -} - -.dictionary .add-btn { - margin-top: 26px; -} - .list .add-btn { - margin-top: 2px; - &.varlist-1st-row { - margin-top: 26px; - } -} - -.right-column .form-group { - padding-left: 0; + margin-bottom: 15px; } .well .panel-body pre { @@ -124,12 +55,10 @@ editable { } i.fa-times-circle { - padding-right: 10px; - .section .section & { - font-weight: bold; - margin-top: 10px; - margin-bottom: 0; - font-size: 15px; - color: inherit; + font-size: 15px; + color: inherit; + + .section .section .section-heading & { + margin-top: 7px; } -} +}; diff --git a/merlin/static/merlin/templates/collapsible-group.html b/merlin/static/merlin/templates/collapsible-group.html index a67b04a..b549d9b 100644 --- a/merlin/static/merlin/templates/collapsible-group.html +++ b/merlin/static/merlin/templates/collapsible-group.html @@ -1,18 +1,18 @@
-
-
+
+
- - {$ group.title() $} + {$ ::title $}
-
+
-
+
diff --git a/merlin/static/merlin/templates/collapsible-panel.html b/merlin/static/merlin/templates/collapsible-panel.html index df21a5a..e321d6f 100644 --- a/merlin/static/merlin/templates/collapsible-panel.html +++ b/merlin/static/merlin/templates/collapsible-panel.html @@ -1,9 +1,11 @@
-
+

- + + {$ ::panel.title $}

diff --git a/merlin/static/merlin/templates/fields/choices.html b/merlin/static/merlin/templates/fields/choices.html index 6f1b25f..6f3f9f8 100644 --- a/merlin/static/merlin/templates/fields/choices.html +++ b/merlin/static/merlin/templates/fields/choices.html @@ -1,16 +1,13 @@ -
- - - -
{$ error $}
-
+ + +
{$ error $}
diff --git a/merlin/static/merlin/templates/fields/dictionary.html b/merlin/static/merlin/templates/fields/dictionary.html index 6d50bb0..33017ac 100644 --- a/merlin/static/merlin/templates/fields/dictionary.html +++ b/merlin/static/merlin/templates/fields/dictionary.html @@ -1,19 +1,34 @@ - -
-
-
-