From f605ff7b8c9c6ee0e7814d57b3ed66e8b03c4332 Mon Sep 17 00:00:00 2001 From: Timur Sufiev Date: Wed, 15 Jul 2015 17:03:21 +0300 Subject: [PATCH] Refactor templates to make them composable The main goal of this change is to free the potential Merlin users from the burden of writing their custom templates when it just involves combining widgets into different levels of nesting. Writing custom templates still remains obligatory when some additional controls/rendering (not provided with built-in widgets) is needed, e.g. YAQLField. To ease the pain of laying out the DOM snippets not known in advance I switched from conventional Bootstrap Grid system to the Flexgrid package which reimplements Bootstrap Grid over CSS3 Flexbox module. It provides all the existing grid features w/o the need to cancel floating effects with div.clearfix and adds pretty vertical/horizontal aligning options which are very useful in Merlin. Besides templates refactoring the filters system was also rewritten. Filter extractPanels() now accepts one required argument, keyExtractor function which is used to calculate a numeric values for every field of Barricade object recursively. The fields with the same numeric values go to the same panel, so we could define the logic of panel extraction separately for each application built on Merlin. For the filters following on the pipeline extractPanels() provides .each() method, which they should use for enumeration over the panel contents. This way the panel implements the same interface as every other Barricade container does. Old extractRows() and extractItems() filters are removed, as well as the necessity to embed positioning hints into the model. As of now precise fields ordering is lost, but will be reimplemented with an extractFields() upgrade (ability to pass a list of field keys is yet to come, as well as the removal of 'index' hints). Implements blueprint: composable-templates Implements blueprint: decouple-ui-hints-and-models Change-Id: I73f480034730099b33afec88cddf919a7bfc441b --- bower.json | 3 +- .../mistral/static/mistral/js/mistral.init.js | 2 +- .../js/mistral.workbook.controllers.js | 14 + .../mistral/js/mistral.workbook.models.js | 112 +- .../mistral/templates/fields/varlist.html | 91 -- .../mistral/templates/fields/yaqlfield.html | 22 + .../mistral/templates/fields/yaqllist.html | 30 - .../mistral/templates/mistral/create.html | 59 +- .../mistral/test/js/workbook.model.spec.js | 7 - merlin/static/merlin/js/merlin.directives.js | 37 +- .../static/merlin/js/merlin.field.models.js | 74 +- merlin/static/merlin/js/merlin.filters.js | 251 ++-- merlin/static/merlin/js/merlin.init.js | 2 +- merlin/static/merlin/js/merlin.utils.js | 18 +- merlin/static/merlin/scss/merlin.scss | 99 +- .../merlin/templates/collapsible-group.html | 12 +- .../merlin/templates/collapsible-panel.html | 6 +- .../merlin/templates/fields/choices.html | 29 +- .../merlin/templates/fields/dictionary.html | 41 +- .../merlin/templates/fields/frozendict.html | 33 +- .../static/merlin/templates/fields/group.html | 13 - .../static/merlin/templates/fields/list.html | 29 +- .../merlin/templates/fields/number.html | 12 +- .../merlin/templates/fields/string.html | 11 +- .../static/merlin/templates/fields/text.html | 11 +- merlin/static/merlin/templates/labeled.html | 4 + merlin/test/js/merlin.directives.spec.js | 2 +- merlin/test/js/merlin.filters.spec.js | 1022 ++++++----------- merlin/test/js/merlin.models.spec.js | 34 +- 29 files changed, 807 insertions(+), 1273 deletions(-) delete mode 100644 extensions/mistral/static/mistral/templates/fields/varlist.html create mode 100644 extensions/mistral/static/mistral/templates/fields/yaqlfield.html delete mode 100644 extensions/mistral/static/mistral/templates/fields/yaqllist.html delete mode 100644 merlin/static/merlin/templates/fields/group.html create mode 100644 merlin/static/merlin/templates/labeled.html 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 @@ - -
-
-
-