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
This commit is contained in:
parent
e17565a708
commit
f605ff7b8c
@ -14,7 +14,8 @@
|
|||||||
"angular-moment": "0.9.0",
|
"angular-moment": "0.9.0",
|
||||||
"angular-cache": "3.2.5",
|
"angular-cache": "3.2.5",
|
||||||
"js-yaml": "3.2.7",
|
"js-yaml": "3.2.7",
|
||||||
"underscore": "1.8.3"
|
"underscore": "1.8.3",
|
||||||
|
"flexboxgrid": "6.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"angular-mocks": "1.3.10",
|
"angular-mocks": "1.3.10",
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
function initModule(templates) {
|
function initModule(templates) {
|
||||||
templates.prefetch('/static/mistral/templates/fields/',
|
templates.prefetch('/static/mistral/templates/fields/',
|
||||||
['varlist', 'yaqllist']);
|
['yaqlfield']);
|
||||||
}
|
}
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
@ -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) {
|
function getNextIDSuffix(container, regexp) {
|
||||||
var max = Math.max.apply(Math, container.getIDs().map(function(id) {
|
var max = Math.max.apply(Math, container.getIDs().map(function(id) {
|
||||||
var match = regexp.exec(id);
|
var match = regexp.exec(id);
|
||||||
|
@ -18,11 +18,15 @@
|
|||||||
if ( angular.isUndefined(json) || type === String ) {
|
if ( angular.isUndefined(json) || type === String ) {
|
||||||
return fields.string.create(json, parameters);
|
return fields.string.create(json, parameters);
|
||||||
} else if ( type === Array ) {
|
} else if ( type === Array ) {
|
||||||
return fields.list.extend({}, {
|
return fields.list.extend({
|
||||||
|
inline: true
|
||||||
|
}, {
|
||||||
'*': {'@class': fields.string}
|
'*': {'@class': fields.string}
|
||||||
}).create(json, parameters);
|
}).create(json, parameters);
|
||||||
} else if ( type === Object ) {
|
} else if ( type === Object ) {
|
||||||
return fields.dictionary.extend({}, {
|
return fields.dictionary.extend({
|
||||||
|
inline: true
|
||||||
|
}, {
|
||||||
'?': {'@class': fields.string}
|
'?': {'@class': fields.string}
|
||||||
}).create(json, parameters);
|
}).create(json, parameters);
|
||||||
}
|
}
|
||||||
@ -31,7 +35,6 @@
|
|||||||
models.varlist = fields.list.extend({
|
models.varlist = fields.list.extend({
|
||||||
create: function(json, parameters) {
|
create: function(json, parameters) {
|
||||||
var self = fields.list.create.call(this, json, parameters);
|
var self = fields.list.create.call(this, json, parameters);
|
||||||
self.setType('varlist');
|
|
||||||
self.on('childChange', function(child, op) {
|
self.on('childChange', function(child, op) {
|
||||||
if ( op == 'empty' ) {
|
if ( op == 'empty' ) {
|
||||||
self.each(function(index, item) {
|
self.each(function(index, item) {
|
||||||
@ -48,6 +51,7 @@
|
|||||||
'@class': fields.frozendict.extend({
|
'@class': fields.frozendict.extend({
|
||||||
create: function(json, parameters) {
|
create: function(json, parameters) {
|
||||||
var self = fields.frozendict.create.call(this, json, parameters);
|
var self = fields.frozendict.create.call(this, json, parameters);
|
||||||
|
self.isAtomic = function() { return false; };
|
||||||
self.on('childChange', function(child) {
|
self.on('childChange', function(child) {
|
||||||
if ( child.instanceof(Barricade.Enumerated) ) { // type change
|
if ( child.instanceof(Barricade.Enumerated) ) { // type change
|
||||||
var value = self.get('value');
|
var value = self.get('value');
|
||||||
@ -87,23 +91,23 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
models.yaqllist = fields.list.extend({
|
models.YAQLField = fields.frozendict.extend({
|
||||||
create: function(json, parameters) {
|
create: function(json, parameters) {
|
||||||
var self = fields.list.create.call(this, json, parameters);
|
var self = fields.frozendict.create.call(this, json, parameters);
|
||||||
self.setType('yaqllist');
|
self.setType('yaqlfield');
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
'*': {
|
|
||||||
'@class': fields.frozendict.extend({}, {
|
|
||||||
'yaql': {
|
'yaql': {
|
||||||
'@class': fields.string
|
'@class': fields.string
|
||||||
},
|
},
|
||||||
'action': {
|
'action': {
|
||||||
'@class': fields.string
|
'@class': fields.string
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
|
||||||
|
models.yaqllist = fields.list.extend({}, {
|
||||||
|
'*': {'@class': models.YAQLField}
|
||||||
});
|
});
|
||||||
|
|
||||||
models.Action = fields.frozendict.extend({
|
models.Action = fields.frozendict.extend({
|
||||||
@ -135,8 +139,7 @@
|
|||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'index': 1,
|
'index': 1
|
||||||
'row': 0
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -144,18 +147,24 @@
|
|||||||
'@class': fields.dictionary.extend({
|
'@class': fields.dictionary.extend({
|
||||||
create: function(json, parameters) {
|
create: function(json, parameters) {
|
||||||
var self = fields.dictionary.create.call(this, json, parameters);
|
var self = fields.dictionary.create.call(this, json, parameters);
|
||||||
|
self.isAdditive = function() { return false; };
|
||||||
self.setType('frozendict');
|
self.setType('frozendict');
|
||||||
return self;
|
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,
|
'@required': false,
|
||||||
'?': {
|
'?': {'@class': fields.string},
|
||||||
'@class': fields.string.extend({}, {
|
|
||||||
'@meta': {
|
|
||||||
'row': 0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'index': 2,
|
'index': 2,
|
||||||
'title': 'Base Input'
|
'title': 'Base Input'
|
||||||
@ -189,9 +198,6 @@
|
|||||||
});
|
});
|
||||||
return self;
|
return self;
|
||||||
},
|
},
|
||||||
remove: function() {
|
|
||||||
this.emit('change', 'taskRemove', this.getID());
|
|
||||||
},
|
|
||||||
_getPrettyJSON: function() {
|
_getPrettyJSON: function() {
|
||||||
var json = fields.frozendict._getPrettyJSON.apply(this, arguments);
|
var json = fields.frozendict._getPrettyJSON.apply(this, arguments);
|
||||||
delete json.type;
|
delete json.type;
|
||||||
@ -200,10 +206,7 @@
|
|||||||
}, {
|
}, {
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'baseKey': 'task',
|
'baseKey': 'task',
|
||||||
'baseName': 'Task ',
|
'baseName': 'Task '
|
||||||
'group': true,
|
|
||||||
'additive': false,
|
|
||||||
'removable': true
|
|
||||||
},
|
},
|
||||||
'type': {
|
'type': {
|
||||||
'@class': fields.string.extend({}, {
|
'@class': fields.string.extend({}, {
|
||||||
@ -214,16 +217,14 @@
|
|||||||
}],
|
}],
|
||||||
'@default': 'action',
|
'@default': 'action',
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'index': 0,
|
'index': 0
|
||||||
'row': 0
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
'description': {
|
'description': {
|
||||||
'@class': fields.text.extend({}, {
|
'@class': fields.text.extend({}, {
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'index': 2,
|
'index': 2
|
||||||
'row': 1
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -268,7 +269,6 @@
|
|||||||
'@required': false,
|
'@required': false,
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'index': 0,
|
'index': 0,
|
||||||
'row': 0,
|
|
||||||
'title': 'Wait before'
|
'title': 'Wait before'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -278,7 +278,6 @@
|
|||||||
'@required': false,
|
'@required': false,
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'index': 1,
|
'index': 1,
|
||||||
'row': 0,
|
|
||||||
'title': 'Wait after'
|
'title': 'Wait after'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -287,8 +286,7 @@
|
|||||||
'@class': fields.number.extend({}, {
|
'@class': fields.number.extend({}, {
|
||||||
'@required': false,
|
'@required': false,
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'index': 2,
|
'index': 2
|
||||||
'row': 1
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -297,7 +295,6 @@
|
|||||||
'@required': false,
|
'@required': false,
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'index': 3,
|
'index': 3,
|
||||||
'row': 2,
|
|
||||||
'title': 'Retry count'
|
'title': 'Retry count'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -307,7 +304,6 @@
|
|||||||
'@required': false,
|
'@required': false,
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'index': 4,
|
'index': 4,
|
||||||
'row': 2,
|
|
||||||
'title': 'Retry delay'
|
'title': 'Retry delay'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -317,7 +313,6 @@
|
|||||||
'@required': false,
|
'@required': false,
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'index': 5,
|
'index': 5,
|
||||||
'row': 3,
|
|
||||||
'title': 'Retry break on'
|
'title': 'Retry break on'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -330,7 +325,6 @@
|
|||||||
'requires': {
|
'requires': {
|
||||||
'@class': fields.string.extend({}, {
|
'@class': fields.string.extend({}, {
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'row': 2,
|
|
||||||
'index': 3
|
'index': 3
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -386,7 +380,6 @@
|
|||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'row': 0,
|
|
||||||
'index': 1
|
'index': 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -407,7 +400,6 @@
|
|||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'row': 0,
|
|
||||||
'index': 1
|
'index': 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -446,8 +438,7 @@
|
|||||||
'@enum': ['reverse', 'direct'],
|
'@enum': ['reverse', 'direct'],
|
||||||
'@default': 'direct',
|
'@default': 'direct',
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'index': 1,
|
'index': 1
|
||||||
'row': 0
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -485,16 +476,13 @@
|
|||||||
var taskData = child.toJSON();
|
var taskData = child.toJSON();
|
||||||
params.id = taskId;
|
params.id = taskId;
|
||||||
self.set(taskPos, TaskFactory(taskData, params));
|
self.set(taskPos, TaskFactory(taskData, params));
|
||||||
} else if ( op === 'taskRemove' ) {
|
|
||||||
self.removeItem(arg);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'index': 5,
|
'index': 5
|
||||||
'group': true
|
|
||||||
},
|
},
|
||||||
'?': {
|
'?': {
|
||||||
'@class': models.Task,
|
'@class': models.Task,
|
||||||
@ -511,9 +499,7 @@
|
|||||||
'@class': fields.frozendict.extend({}, {
|
'@class': fields.frozendict.extend({}, {
|
||||||
'@required': false,
|
'@required': false,
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'index': 4,
|
'index': 4
|
||||||
'group': true,
|
|
||||||
'additive': false
|
|
||||||
},
|
},
|
||||||
'on-error': {
|
'on-error': {
|
||||||
'@class': models.yaqllist.extend({}, {
|
'@class': models.yaqllist.extend({}, {
|
||||||
@ -557,8 +543,7 @@
|
|||||||
models.Actions = fields.dictionary.extend({}, {
|
models.Actions = fields.dictionary.extend({}, {
|
||||||
'@required': false,
|
'@required': false,
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'index': 3,
|
'index': 3
|
||||||
'panelIndex': 1
|
|
||||||
},
|
},
|
||||||
'?': {
|
'?': {
|
||||||
'@class': models.Action
|
'@class': models.Action
|
||||||
@ -583,8 +568,7 @@
|
|||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'index': 4,
|
'index': 4
|
||||||
'panelIndex': 2
|
|
||||||
},
|
},
|
||||||
'?': {
|
'?': {
|
||||||
'@class': models.Workflow,
|
'@class': models.Workflow,
|
||||||
@ -601,9 +585,7 @@
|
|||||||
'@class': fields.string.extend({}, {
|
'@class': fields.string.extend({}, {
|
||||||
'@enum': ['2.0'],
|
'@enum': ['2.0'],
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'index': 2,
|
'index': 2
|
||||||
'panelIndex': 0,
|
|
||||||
'row': 1
|
|
||||||
},
|
},
|
||||||
'@default': '2.0'
|
'@default': '2.0'
|
||||||
})
|
})
|
||||||
@ -611,9 +593,7 @@
|
|||||||
'name': {
|
'name': {
|
||||||
'@class': fields.string.extend({}, {
|
'@class': fields.string.extend({}, {
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'index': 0,
|
'index': 0
|
||||||
'panelIndex': 0,
|
|
||||||
'row': 0
|
|
||||||
},
|
},
|
||||||
'@constraints': [
|
'@constraints': [
|
||||||
function(value) {
|
function(value) {
|
||||||
@ -625,9 +605,7 @@
|
|||||||
'description': {
|
'description': {
|
||||||
'@class': fields.text.extend({}, {
|
'@class': fields.text.extend({}, {
|
||||||
'@meta': {
|
'@meta': {
|
||||||
'index': 1,
|
'index': 1
|
||||||
'panelIndex': 0,
|
|
||||||
'row': 0
|
|
||||||
},
|
},
|
||||||
'@required': false
|
'@required': false
|
||||||
})
|
})
|
||||||
|
@ -1,91 +0,0 @@
|
|||||||
<collapsible-group content="value"
|
|
||||||
on-add="value.add()">
|
|
||||||
<div class="three-columns" ng-repeat="subItem in value.getValues() track by $index"
|
|
||||||
ng-class="subItem.get('type').get()">
|
|
||||||
<div class="left-column">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="elem-{$ $id $}.$index">Key Type</label>
|
|
||||||
<select id="elem-{$ $id $}.$index" class="form-control"
|
|
||||||
ng-model="subItem.get('type').value" ng-model-options="{getterSetter: true}">
|
|
||||||
<option ng-repeat="value in subItem.get('type').getEnumValues()"
|
|
||||||
value="{$ value $}"
|
|
||||||
ng-selected="subItem.get('type').get() == value">{$ value $}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div ng-switch="subItem.get('type').value()">
|
|
||||||
<!-- draw string input -->
|
|
||||||
<div class="right-column" ng-switch-when="string">
|
|
||||||
<div class="form-group">
|
|
||||||
<label> </label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" class="form-control"
|
|
||||||
ng-model="subItem.get('value').value" ng-model-options="{getterSetter: true}">
|
|
||||||
<span class="input-group-btn">
|
|
||||||
<button class="btn btn-default" ng-click="value.remove($index)">
|
|
||||||
<i class="fa fa-minus-circle"></i>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- END: draw string input -->
|
|
||||||
<!-- draw dictionary inputs -->
|
|
||||||
<div ng-switch-when="dictionary">
|
|
||||||
<div ng-repeat="(key, value) in subItem.get('value').getValues() track by key">
|
|
||||||
<div ng-hide="$first" class="left-column"></div>
|
|
||||||
<div class="right-column">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="elem-{$ $id $}.{$ key $}">
|
|
||||||
<editable ng-model="value.keyValue" ng-model-options="{getterSetter: true}"></editable>
|
|
||||||
</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" id="elem-{$ $id $}.{$ key $}" class="form-control" ng-model="value.value"
|
|
||||||
ng-model-options="{getterSetter: true}">
|
|
||||||
<span class="input-group-btn">
|
|
||||||
<button class="btn btn-default" ng-click="subItem.get('value').remove(key)">
|
|
||||||
<i class="fa fa-minus-circle"></i>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div ng-hide="$last" class="clearfix"></div>
|
|
||||||
<div ng-show="$last" class="add-btn button-column">
|
|
||||||
<button class="btn btn-default btn-sm pull-right" ng-click="subItem.get('value').add()">
|
|
||||||
<i class="fa fa-plus"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- END: draw dictionary inputs -->
|
|
||||||
<!-- draw list inputs -->
|
|
||||||
<div ng-switch-when="list">
|
|
||||||
<div ng-repeat="value in subItem.get('value').getValues() track by $index">
|
|
||||||
<div ng-hide="$first" class="left-column"></div>
|
|
||||||
<div class="right-column">
|
|
||||||
<div class="form-group">
|
|
||||||
<label ng-show="$first"> </label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" class="form-control" ng-model="value.value"
|
|
||||||
ng-model-options="{getterSetter: true}">
|
|
||||||
<span class="input-group-btn">
|
|
||||||
<button class="btn btn-default" ng-click="subItem.get('value').remove($index)">
|
|
||||||
<i class="fa fa-minus-circle"></i>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div ng-hide="$last" class="clearfix"></div>
|
|
||||||
<div ng-show="$last" class="add-btn button-column" ng-class="{'varlist-1st-row': !$index}">
|
|
||||||
<button class="btn btn-default btn-sm pull-right" ng-click="subItem.get('value').add()">
|
|
||||||
<i class="fa fa-plus"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- END: draw list inputs -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</collapsible-group>
|
|
@ -0,0 +1,22 @@
|
|||||||
|
<div class="row">
|
||||||
|
<div class="col-xs" ng-show="value.showYaql">
|
||||||
|
<div class="form-group">
|
||||||
|
<textarea class="form-control" ng-model="value.get('yaql').value"
|
||||||
|
ng-model-options="{getterSetter: true}"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button class="btn btn-default" ng-click="value.showYaql = !value.showYaql;">
|
||||||
|
<i class="fa"
|
||||||
|
ng-class="{'fa-lock': value.get('yaql').value(), 'fa-unlock': !value.get('yaql').value()}"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<input type="text" class="form-control" ng-model="value.get('action').value"
|
||||||
|
ng-model-options="{getterSetter: true}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,30 +0,0 @@
|
|||||||
<collapsible-group content="value" on-add="value.add()">
|
|
||||||
<div class="three-columns"
|
|
||||||
ng-repeat="subItem in value.getValues() track by $index">
|
|
||||||
<div class="left-column" ng-show="subItem.showYaql">
|
|
||||||
<div class="form-group">
|
|
||||||
<textarea class="form-control" ng-model="subItem.get('yaql').value"
|
|
||||||
ng-model-options="{getterSetter: true}"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="right-column">
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="input-group">
|
|
||||||
<span class="input-group-btn">
|
|
||||||
<button class="btn btn-default" ng-click="subItem.showYaql = !subItem.showYaql;">
|
|
||||||
<i class="fa"
|
|
||||||
ng-class="{'fa-lock': subItem.get('yaql').value(), 'fa-unlock': !subItem.get('yaql').value()}"></i>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
<input type="text" class="form-control" ng-model="subItem.get('action').value"
|
|
||||||
ng-model-options="{getterSetter: true}">
|
|
||||||
<span class="input-group-btn">
|
|
||||||
<button class="btn btn-default" ng-click="value.remove($index)">
|
|
||||||
<i class="fa fa-minus-circle"></i>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</collapsible-group>
|
|
@ -33,21 +33,22 @@
|
|||||||
{% compress css %}
|
{% compress css %}
|
||||||
<link href='{{ STATIC_URL }}merlin/scss/merlin.scss' type='text/scss' media='screen' rel='stylesheet' />
|
<link href='{{ STATIC_URL }}merlin/scss/merlin.scss' type='text/scss' media='screen' rel='stylesheet' />
|
||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
|
<link href='{{ STATIC_URL }}merlin/libs/flexboxgrid/dist/flexboxgrid.css' type='text/css' media='screen' rel='stylesheet' />
|
||||||
{% block merlin-css %}{% endblock %}
|
{% block merlin-css %}{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<h3>Create Workbook</h3>
|
<h3>Create Workbook</h3>
|
||||||
<div id="create-workbook" class="fluid-container" ng-cloak ng-controller="WorkbookController as wb"
|
<div id="create-workbook" ng-cloak ng-controller="WorkbookController as wb"
|
||||||
ng-init="wb.init({{ id|default:'undefined' }}, '{{ yaml }}', '{{ commit_url }}', '{{ discard_url }}')">
|
ng-init="wb.init({{ id|default:'undefined' }}, '{{ yaml }}', '{{ commit_url }}', '{{ discard_url }}')">
|
||||||
<div class="well">
|
<div class="well">
|
||||||
<div class="two-panels">
|
<div class="row">
|
||||||
<div class="left-panel">
|
<div class="col-xs row">
|
||||||
<div class="pull-left">
|
<div class="col-xs start-xs">
|
||||||
<h4><strong>{$ wb.workbook.get('name') $}</strong></h4>
|
<h4><strong>{$ wb.workbook.get('name') $}</strong></h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right">
|
<div class="col-xs end-xs">
|
||||||
<div class="table-actions clearfix">
|
<div class="table-actions">
|
||||||
<button ng-click="wb.addAction()" class="btn btn-default btn-sm">
|
<button ng-click="wb.addAction()" class="btn btn-default btn-sm">
|
||||||
<span class="fa fa-plus">Add Action</span></button>
|
<span class="fa fa-plus">Add Action</span></button>
|
||||||
<button ng-click="wb.addWorkflow()" class="btn btn-default btn-sm">
|
<button ng-click="wb.addWorkflow()" class="btn btn-default btn-sm">
|
||||||
@ -55,8 +56,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-panel">
|
<div class="col-xs end-xs">
|
||||||
<div class="btn-group btn-toggle pull-right">
|
<div class="btn-group btn-toggle">
|
||||||
<button ng-click="wb.isGraphMode = true" class="btn btn-sm"
|
<button ng-click="wb.isGraphMode = true" class="btn btn-sm"
|
||||||
ng-class="wb.isGraphMode ? 'active btn-primary' : 'btn-default'">Graph</button>
|
ng-class="wb.isGraphMode ? 'active btn-primary' : 'btn-default'">Graph</button>
|
||||||
<button ng-click="wb.isGraphMode = false" class="btn btn-sm"
|
<button ng-click="wb.isGraphMode = false" class="btn btn-sm"
|
||||||
@ -65,23 +66,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Data panel start -->
|
<!-- Data panel start -->
|
||||||
<div class="two-panels">
|
<div class="row">
|
||||||
<div class="left-panel">
|
<div class="col-xs">
|
||||||
<panel ng-repeat="panel in wb.workbook | extractPanels track by panel.id"
|
<panel ng-repeat="panel in wb.workbook | extractPanels:wb.keyExtractor track by panel.id"
|
||||||
content="panel">
|
content="panel">
|
||||||
<div ng-repeat="row in panel | extractRows track by row.id">
|
<div ng-repeat="row in panel | extractFields | chunks:2 track by $index">
|
||||||
<div ng-class="{'two-columns': row.index !== undefined }">
|
<div ng-repeat="(label, field) in row track by field.uid()">
|
||||||
<div ng-repeat="item in row | extractItems track by item.id"
|
<div ng-if="field.isAtomic()" class="col-xs-6">
|
||||||
ng-class="{'right-column': item.isAtomic() && $odd, 'left-column': item.isAtomic() && $even}">
|
<labeled label="{$ label $}" for="{$ field.uid() $}">
|
||||||
<typed-field value="item" type="{$ item.getType() $}"></typed-field>
|
<typed-field value="field" type="{$ field.getType() $}"></typed-field>
|
||||||
<div class="clearfix" ng-if="$odd"></div>
|
</labeled>
|
||||||
|
</div>
|
||||||
|
<div ng-if="!field.isAtomic()" class="col-xs-12">
|
||||||
|
<collapsible-group content="field" title="label"
|
||||||
|
additive="{$ field.isAdditive() $}" on-add="field.add()">
|
||||||
|
<div ng-class="field.isPlainStructure() ? 'col-xs-6' : 'col-xs-12'">
|
||||||
|
<typed-field value="field" type="{$ field.getType() $}"></typed-field>
|
||||||
|
</div>
|
||||||
|
</collapsible-group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</panel>
|
</panel>
|
||||||
</div>
|
</div>
|
||||||
<!-- YAML Panel -->
|
<!-- YAML Panel -->
|
||||||
<div class="right-panel">
|
<div class="col-xs">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-body" ng-show="!wb.isGraphMode">
|
<div class="panel-body" ng-show="!wb.isGraphMode">
|
||||||
<pre>{$ wb.workbook.toYAML() $}</pre>
|
<pre>{$ wb.workbook.toYAML() $}</pre>
|
||||||
@ -93,9 +102,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- page footer -->
|
<!-- page footer -->
|
||||||
<div class="two-panels">
|
<div class="row">
|
||||||
<div class="full-width">
|
<div class="col-xs end-xs">
|
||||||
<div class="pull-right">
|
|
||||||
<button ng-click="wb.discardWorkbook()" class="btn btn-default cancel">Cancel</button>
|
<button ng-click="wb.discardWorkbook()" class="btn btn-default cancel">Cancel</button>
|
||||||
<button ng-click="wb.commitWorkbook()" class="btn btn-primary">
|
<button ng-click="wb.commitWorkbook()" class="btn btn-primary">
|
||||||
{$ wb.workbookID ? 'Modify' : 'Create' $}
|
{$ wb.workbookID ? 'Modify' : 'Create' $}
|
||||||
@ -104,5 +112,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -129,13 +129,6 @@ describe('workbook model logic', function() {
|
|||||||
expect(json.workflows[workflowID].tasks[newID]).toBeDefined();
|
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() {
|
describe("which start with the 'direct' workflow:", function() {
|
||||||
|
@ -39,9 +39,21 @@
|
|||||||
* retrieves a template by its name which is the same as model's type and renders it,
|
* retrieves a template by its name which is the same as model's type and renders it,
|
||||||
* recursive <typed-field></..>-s are possible.
|
* recursive <typed-field></..>-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() {
|
function editable() {
|
||||||
return {
|
return {
|
||||||
@ -100,6 +112,7 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showFocus.$inject = ['$timeout'];
|
||||||
function showFocus($timeout) {
|
function showFocus($timeout) {
|
||||||
return function(scope, element, attrs) {
|
return function(scope, element, attrs) {
|
||||||
// Unused variable created here due to rule 'ng_on_watch': 2
|
// Unused variable created here due to rule 'ng_on_watch': 2
|
||||||
@ -114,7 +127,7 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function panel($parse) {
|
function panel() {
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
templateUrl: '/static/merlin/templates/collapsible-panel.html',
|
templateUrl: '/static/merlin/templates/collapsible-panel.html',
|
||||||
@ -122,9 +135,13 @@
|
|||||||
scope: {
|
scope: {
|
||||||
panel: '=content'
|
panel: '=content'
|
||||||
},
|
},
|
||||||
link: function(scope, element, attrs) {
|
link: function(scope) {
|
||||||
scope.removable = $parse(attrs.removable)();
|
if (angular.isDefined(scope.panel)) {
|
||||||
scope.isCollapsed = false;
|
scope.isCollapsed = false;
|
||||||
|
if (angular.isFunction(scope.panel.title)) {
|
||||||
|
scope.editable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -136,11 +153,15 @@
|
|||||||
transclude: true,
|
transclude: true,
|
||||||
scope: {
|
scope: {
|
||||||
group: '=content',
|
group: '=content',
|
||||||
|
title: '=',
|
||||||
onAdd: '&',
|
onAdd: '&',
|
||||||
onRemove: '&'
|
onRemove: '&'
|
||||||
},
|
},
|
||||||
link: function(scope, element, attrs) {
|
link: function(scope, element, attrs) {
|
||||||
scope.isCollapsed = false;
|
scope.isCollapsed = false;
|
||||||
|
if (angular.isFunction(scope.title)) {
|
||||||
|
scope.editable = true;
|
||||||
|
}
|
||||||
if ( attrs.onAdd && attrs.additive !== 'false' ) {
|
if ( attrs.onAdd && attrs.additive !== 'false' ) {
|
||||||
scope.additive = true;
|
scope.additive = true;
|
||||||
}
|
}
|
||||||
@ -151,6 +172,7 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validatableWith.$inject = ['$parse'];
|
||||||
function validatableWith($parse) {
|
function validatableWith($parse) {
|
||||||
return {
|
return {
|
||||||
restrict: 'A',
|
restrict: 'A',
|
||||||
@ -186,6 +208,7 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedField.$inject = ['$compile', 'merlin.templates'];
|
||||||
function typedField($compile, templates) {
|
function typedField($compile, templates) {
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
@ -195,7 +218,7 @@
|
|||||||
},
|
},
|
||||||
link: function(scope, element) {
|
link: function(scope, element) {
|
||||||
templates.templateReady(scope.type).then(function(template) {
|
templates.templateReady(scope.type).then(function(template) {
|
||||||
element.replaceWith($compile(template)(scope));
|
element.append($compile(template)(scope));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -61,6 +61,24 @@
|
|||||||
return this;
|
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 modelMixin = Barricade.Blueprint.create(function(type) {
|
||||||
var isValid = true;
|
var isValid = true;
|
||||||
var isValidatable = false;
|
var isValidatable = false;
|
||||||
@ -90,8 +108,12 @@
|
|||||||
type = _type;
|
type = _type;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.isAdditive = function() {
|
||||||
|
return this.instanceof(Barricade.Arraylike);
|
||||||
|
};
|
||||||
|
|
||||||
this.isAtomic = function() {
|
this.isAtomic = function() {
|
||||||
return ['number', 'string', 'text', 'choices'].indexOf(this.getType()) > -1;
|
return !this.instanceof(Barricade.Container);
|
||||||
};
|
};
|
||||||
this.title = function() {
|
this.title = function() {
|
||||||
var title = utils.getMeta(this, 'title');
|
var title = utils.getMeta(this, 'title');
|
||||||
@ -148,13 +170,8 @@
|
|||||||
self.add = function() {
|
self.add = function() {
|
||||||
self.push(undefined, parameters);
|
self.push(undefined, parameters);
|
||||||
};
|
};
|
||||||
self.getValues = function() {
|
|
||||||
return self.toArray();
|
|
||||||
};
|
|
||||||
self._getContents = function() {
|
|
||||||
return self.toArray();
|
|
||||||
};
|
|
||||||
meldGroup.call(self);
|
meldGroup.call(self);
|
||||||
|
plainStructureMixin.call(self);
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
}, {'@type': Array});
|
}, {'@type': Array});
|
||||||
@ -162,20 +179,10 @@
|
|||||||
var frozendictModel = Barricade.ImmutableObject.extend({
|
var frozendictModel = Barricade.ImmutableObject.extend({
|
||||||
create: function(json, parameters) {
|
create: function(json, parameters) {
|
||||||
var self = Barricade.ImmutableObject.create.call(this, 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');
|
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);
|
meldGroup.call(self);
|
||||||
|
plainStructureMixin.call(self);
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
}, {'@type': Object});
|
}, {'@type': Object});
|
||||||
@ -183,15 +190,14 @@
|
|||||||
var dictionaryModel = Barricade.MutableObject.extend({
|
var dictionaryModel = Barricade.MutableObject.extend({
|
||||||
create: function(json, parameters) {
|
create: function(json, parameters) {
|
||||||
var self = Barricade.MutableObject.create.call(this, json, parameters);
|
var self = Barricade.MutableObject.create.call(this, json, parameters);
|
||||||
var _items = [];
|
|
||||||
var _elClass = self._elementClass;
|
var _elClass = self._elementClass;
|
||||||
var baseKey = utils.getMeta(_elClass, 'baseKey') || 'key';
|
var baseKey = utils.getMeta(_elClass, 'baseKey') || 'key';
|
||||||
var baseName = utils.getMeta(_elClass, 'baseName') || utils.makeTitle(baseKey);
|
var baseName = utils.getMeta(_elClass, 'baseName') || utils.makeTitle(baseKey);
|
||||||
|
|
||||||
modelMixin.call(self, 'dictionary');
|
modelMixin.call(self, 'dictionary');
|
||||||
|
plainStructureMixin.call(self);
|
||||||
|
|
||||||
function makeCacheWrapper(container, key) {
|
function initKeyAccessor(value) {
|
||||||
var value = container.getByID(key);
|
|
||||||
value.keyValue = function () {
|
value.keyValue = function () {
|
||||||
if ( arguments.length ) {
|
if ( arguments.length ) {
|
||||||
value.setID(arguments[0]);
|
value.setID(arguments[0]);
|
||||||
@ -199,9 +205,16 @@
|
|||||||
return value.getID();
|
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) {
|
self.add = function(newID) {
|
||||||
var regexp = new RegExp('(' + baseKey + ')([0-9]+)');
|
var regexp = new RegExp('(' + baseKey + ')([0-9]+)');
|
||||||
var newValue;
|
var newValue;
|
||||||
@ -217,21 +230,11 @@
|
|||||||
newValue = '';
|
newValue = '';
|
||||||
}
|
}
|
||||||
self.push(newValue, utils.extend(self._parameters, {id: newID}));
|
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() {
|
self.empty = function() {
|
||||||
for ( var i = this._data.length; i > 0; i-- ) {
|
for ( var i = this._data.length; i > 0; i-- ) {
|
||||||
self.remove(i - 1);
|
self.remove(i - 1);
|
||||||
}
|
}
|
||||||
_items = [];
|
|
||||||
};
|
};
|
||||||
self.resetKeys = function(keys) {
|
self.resetKeys = function(keys) {
|
||||||
self.empty();
|
self.empty();
|
||||||
@ -239,17 +242,10 @@
|
|||||||
self.push(undefined, {id: key});
|
self.push(undefined, {id: key});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
self._getContents = function() {
|
|
||||||
return self.toArray();
|
|
||||||
};
|
|
||||||
self.removeItem = function(key) {
|
self.removeItem = function(key) {
|
||||||
var pos = self.getPosByID(key);
|
|
||||||
self.remove(self.getPosByID(key));
|
self.remove(self.getPosByID(key));
|
||||||
_items.splice(pos, 1);
|
|
||||||
};
|
};
|
||||||
meldGroup.call(self);
|
meldGroup.call(self);
|
||||||
// initialize cache with starting values
|
|
||||||
self.getValues();
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
}, {'@type': Object});
|
}, {'@type': Object});
|
||||||
|
@ -16,148 +16,187 @@
|
|||||||
(function() {
|
(function() {
|
||||||
angular
|
angular
|
||||||
.module('merlin')
|
.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('extractPanels', extractPanels)
|
||||||
.filter('extractRows', extractRows)
|
.filter('extractFields', extractFields)
|
||||||
.filter('extractItems', extractItems);
|
.filter('chunks', chunks);
|
||||||
|
|
||||||
extractPanels.$inject = ['merlin.utils'];
|
extractPanels.$inject = ['merlin.utils'];
|
||||||
extractRows.$inject = ['merlin.utils'];
|
|
||||||
extractItems.$inject = ['merlin.utils'];
|
|
||||||
|
|
||||||
function extractPanels(utils) {
|
function extractPanels(utils) {
|
||||||
var panelProto = {
|
var panelProto = {
|
||||||
create: function(itemsOrContainer, id) {
|
create: function(enumerator, obj, context) {
|
||||||
if ( angular.isArray(itemsOrContainer) && !itemsOrContainer.length ) {
|
this.$$obj = obj;
|
||||||
return null;
|
this.$$enumerator = enumerator;
|
||||||
}
|
this.removable = false;
|
||||||
if ( angular.isArray(itemsOrContainer) ) {
|
if (this.$$obj) {
|
||||||
this.items = itemsOrContainer;
|
this.id = this.$$obj.uid();
|
||||||
this.id = itemsOrContainer.reduce(function(prevId, item) {
|
this.$$objParent = context.container;
|
||||||
return item.uid() + prevId;
|
this.removable = this.$$objParent.instanceof(Barricade.Arraylike);
|
||||||
}, '');
|
if (this.$$objParent.instanceof(Barricade.MutableObject)) {
|
||||||
|
this.title = function() {
|
||||||
|
if ( arguments.length ) {
|
||||||
|
obj.setID(arguments[0]);
|
||||||
} else {
|
} else {
|
||||||
this._barricadeContainer = itemsOrContainer;
|
return obj.getID();
|
||||||
this._barricadeId = id;
|
}
|
||||||
var barricadeObj = itemsOrContainer.getByID(id);
|
};
|
||||||
this.id = barricadeObj.uid();
|
} else if (this.$$objParent.instanceof(Barricade.ImmutableObject)) {
|
||||||
this.items = barricadeObj.getKeys().map(function(key) {
|
this.title = context.indexOrKey;
|
||||||
return utils.enhanceItemWithID(barricadeObj.get(key), key);
|
}
|
||||||
|
} else {
|
||||||
|
var id = '';
|
||||||
|
this.$$enumerator(function(key, item) {
|
||||||
|
id += item.uid();
|
||||||
});
|
});
|
||||||
this.removable = true;
|
this.id = id;
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
title: function() {
|
each: function(callback, comparator) {
|
||||||
var newID;
|
this.$$enumerator.call(this.$$obj, callback, comparator);
|
||||||
if ( this._barricadeContainer ) {
|
|
||||||
if ( arguments.length ) {
|
|
||||||
newID = arguments[0];
|
|
||||||
this._barricadeContainer.getByID(this._barricadeId).setID(newID);
|
|
||||||
this._barricadeId = newID;
|
|
||||||
} else {
|
|
||||||
return this._barricadeId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
remove: function() {
|
remove: function() {
|
||||||
var container = this._barricadeContainer;
|
var index;
|
||||||
var pos = container.getPosByID(this._barricadeId);
|
if (this.removable) {
|
||||||
container.remove(pos);
|
index = this.$$objParent.toArray().indexOf(this.$$obj);
|
||||||
|
this.$$objParent.remove(index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function isPanelsRoot(item) {
|
return _.memoize(function(container, keyExtractor) {
|
||||||
try {
|
var items = [];
|
||||||
// check for 'actions' and 'workflows' containers
|
var _data = {};
|
||||||
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();
|
|
||||||
var panels = [];
|
var panels = [];
|
||||||
utils.groupByMetaKey(items, 'panelIndex').forEach(function(items) {
|
|
||||||
var panelsRoot = extractPanelsRoot(items);
|
/* This function recursively applies determinant 'keyExtractor' function
|
||||||
if ( panelsRoot ) {
|
to each container (given that the determinant doesn't return a numeric
|
||||||
panelsRoot.getIDs().forEach(function(id) {
|
value for it), starting from the top-level. Fields for which determinant
|
||||||
panels.push(Object.create(panelProto).create(panelsRoot, id));
|
returns a numeric value, will be later placed into a panels (see docs for
|
||||||
});
|
'extractPanels' filter).
|
||||||
} else {
|
*/
|
||||||
panels.push(Object.create(panelProto).create(items));
|
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 {
|
||||||
|
obj = items[0];
|
||||||
|
enumerator = obj.each;
|
||||||
|
context = _data[obj.uid()];
|
||||||
|
}
|
||||||
|
panels.push(Object.create(panelProto).create(enumerator, obj, context));
|
||||||
|
});
|
||||||
return utils.condense(panels);
|
return utils.condense(panels);
|
||||||
}, function(container) {
|
}, function(container, keyExtractor) {
|
||||||
var hash = '';
|
var hash = '';
|
||||||
container.getKeys().map(function(key) {
|
function rec(container) {
|
||||||
var item = container.get(key);
|
container.each(function(indexOrKey, item) {
|
||||||
if ( isPanelsRoot(item) ) {
|
var groupingKey = keyExtractor(item, container);
|
||||||
item.getIDs().forEach(function(id) {
|
if (angular.isNumber(groupingKey)) {
|
||||||
hash += item.getByID(id).uid();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
hash += item.uid();
|
hash += item.uid();
|
||||||
|
} else if (item.instanceof(Barricade.Container)) {
|
||||||
|
rec(item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
rec(container);
|
||||||
return hash;
|
return hash;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractRows(utils) {
|
function extractFields() {
|
||||||
function getItems(panelOrContainer) {
|
return _.memoize(function(container) {
|
||||||
if ( panelOrContainer.items ) {
|
var fields = {};
|
||||||
return panelOrContainer.items;
|
container.each(function(key, item) {
|
||||||
} else if ( panelOrContainer.getKeys ) {
|
fields[key] = item;
|
||||||
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 = 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);
|
|
||||||
});
|
});
|
||||||
|
return fields;
|
||||||
}, function(panel) {
|
}, function(panel) {
|
||||||
var hash = '';
|
var hash = '';
|
||||||
getItems(panel).forEach(function(item) {
|
panel.each(function(key, item) {
|
||||||
hash += item.uid();
|
hash += item.uid();
|
||||||
});
|
});
|
||||||
return hash;
|
return hash;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractItems(utils) {
|
function chunks() {
|
||||||
return _.memoize(function(row) {
|
return _.memoize(function(fields, itemsPerChunk) {
|
||||||
return row.items.sort(function(item1, item2) {
|
var chunks = [];
|
||||||
return utils.getMeta(item1, 'index') - utils.getMeta(item2, 'index');
|
var keys = Object.keys(fields);
|
||||||
});
|
var i, j, chunk;
|
||||||
}, function(row) {
|
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 = '';
|
var hash = '';
|
||||||
row.items.forEach(function(item) {
|
var key;
|
||||||
hash += item.uid();
|
for (key in fields) {
|
||||||
});
|
if (fields.hasOwnProperty(key)) {
|
||||||
|
hash += fields[key].uid();
|
||||||
|
}
|
||||||
|
}
|
||||||
return hash;
|
return hash;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
function fieldTemplates() {
|
function fieldTemplates() {
|
||||||
return [
|
return [
|
||||||
'dictionary', 'frozendict', 'list',
|
'dictionary', 'frozendict', 'list',
|
||||||
'string', 'text', 'group', 'number', 'choices'
|
'string', 'text', 'number', 'choices'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,16 +23,16 @@
|
|||||||
return 'id-' + idCounter;
|
return 'id-' + idCounter;
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupByMetaKey(sequence, metaKey, insertAtBeginning) {
|
function groupByExtractedKey(sequence, keyExtractor, insertAtBeginning) {
|
||||||
var newSequence = [];
|
var newSequence = [];
|
||||||
var defaultBucket = [];
|
var defaultBucket = [];
|
||||||
var index;
|
var index;
|
||||||
sequence.forEach(function(item) {
|
sequence.forEach(function(item) {
|
||||||
index = getMeta(item, metaKey);
|
index = keyExtractor(item);
|
||||||
if ( angular.isDefined(index) ) {
|
if ( angular.isDefined(index) ) {
|
||||||
if ( !newSequence[index] ) {
|
if ( !newSequence[index] ) {
|
||||||
newSequence[index] = [];
|
newSequence[index] = [];
|
||||||
newSequence[index][metaKey] = index;
|
newSequence[index][keyExtractor()] = index;
|
||||||
}
|
}
|
||||||
newSequence[index].push(item);
|
newSequence[index].push(item);
|
||||||
} else {
|
} else {
|
||||||
@ -51,6 +51,17 @@
|
|||||||
return newSequence;
|
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) {
|
function getMeta(item, key) {
|
||||||
if ( item ) {
|
if ( item ) {
|
||||||
var meta = item._schema['@meta'];
|
var meta = item._schema['@meta'];
|
||||||
@ -103,6 +114,7 @@
|
|||||||
getMeta: getMeta,
|
getMeta: getMeta,
|
||||||
getNewId: getNewId,
|
getNewId: getNewId,
|
||||||
groupByMetaKey: groupByMetaKey,
|
groupByMetaKey: groupByMetaKey,
|
||||||
|
groupByExtractedKey: groupByExtractedKey,
|
||||||
makeTitle: makeTitle,
|
makeTitle: makeTitle,
|
||||||
getNextIDSuffix: getNextIDSuffix,
|
getNextIDSuffix: getNextIDSuffix,
|
||||||
enhanceItemWithID: enhanceItemWithID,
|
enhanceItemWithID: enhanceItemWithID,
|
||||||
|
@ -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-default.merlin-panel {
|
||||||
.panel-heading {
|
.panel-heading {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
border: none;
|
border: none;
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
.panel-body {
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
}
|
||||||
textarea {
|
textarea {
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
@ -64,19 +19,15 @@ editable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
.form-group {
|
h5 {
|
||||||
padding-left: 15px;
|
font-weight: bold;
|
||||||
}
|
|
||||||
.section {
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-heading {
|
||||||
a {
|
a {
|
||||||
padding-left: 5px;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
h5 {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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 {
|
.list .add-btn {
|
||||||
margin-top: 2px;
|
margin-bottom: 15px;
|
||||||
&.varlist-1st-row {
|
|
||||||
margin-top: 26px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-column .form-group {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.well .panel-body pre {
|
.well .panel-body pre {
|
||||||
@ -124,12 +55,10 @@ editable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
i.fa-times-circle {
|
i.fa-times-circle {
|
||||||
padding-right: 10px;
|
|
||||||
.section .section & {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 0;
|
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
|
||||||
|
.section .section .section-heading & {
|
||||||
|
margin-top: 7px;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-heading three-columns">
|
<div class="section-heading row">
|
||||||
<div class="both-columns">
|
<div class="col-xs-10">
|
||||||
<h5><a ng-click="isCollapsed = !isCollapsed" class="collapse-entries" href="">
|
<h5><a ng-click="isCollapsed = !isCollapsed" class="collapse-entries" href="">
|
||||||
<i class="fa" ng-class="isCollapsed ? 'fa-plus-square-o' : 'fa-minus-square-o'"></i></a>
|
<i class="fa" ng-class="isCollapsed ? 'fa-plus-square-o' : 'fa-minus-square-o'"></i></a>
|
||||||
<editable ng-if="removable" ng-model="group.title"
|
<editable ng-if="editable" ng-model="title"
|
||||||
ng-model-options="{getterSetter: true}"></editable>
|
ng-model-options="{getterSetter: true}"></editable>
|
||||||
<span ng-if="!removable">{$ group.title() $}</span>
|
<span ng-if="!editable">{$ ::title $}</span>
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="additive" class="add-btn button-column add-entry">
|
<div ng-if="additive" class="add-btn col-xs add-entry">
|
||||||
<button class="btn btn-default btn-sm pull-right" ng-click="onAdd()">
|
<button class="btn btn-default btn-sm pull-right" ng-click="onAdd()">
|
||||||
<i class="fa fa-plus"></i></button>
|
<i class="fa fa-plus"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="removable" class="add-btn button-column remove-entry">
|
<div ng-if="removable" class="add-btn col-xs remove-entry">
|
||||||
<a href="" ng-click="onRemove()">
|
<a href="" ng-click="onRemove()">
|
||||||
<i class="fa fa-times-circle pull-right"></i></a>
|
<i class="fa fa-times-circle pull-right"></i></a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
<div class="panel panel-default merlin-panel">
|
<div class="panel panel-default merlin-panel">
|
||||||
<div class="panel-heading" ng-show="panel.title()">
|
<div class="panel-heading" ng-show="panel.title">
|
||||||
<h4 class="panel-title">
|
<h4 class="panel-title">
|
||||||
<a ng-click="isCollapsed = !isCollapsed" href="">
|
<a ng-click="isCollapsed = !isCollapsed" href="">
|
||||||
<i class="fa fa-lg" ng-class="isCollapsed ? 'fa-caret-right' : 'fa-caret-down'"></i></a>
|
<i class="fa fa-lg" ng-class="isCollapsed ? 'fa-caret-right' : 'fa-caret-down'"></i></a>
|
||||||
<editable ng-model="panel.title" ng-model-options="{getterSetter: true}"></editable>
|
<editable ng-if="editable" ng-model="panel.title"
|
||||||
|
ng-model-options="{getterSetter: true}"></editable>
|
||||||
|
<span ng-if="!editable">{$ ::panel.title $}</span>
|
||||||
<a href="" ng-show="panel.removable" ng-click="panel.remove()">
|
<a href="" ng-show="panel.removable" ng-click="panel.remove()">
|
||||||
<i class="fa fa-times-circle pull-right"></i></a>
|
<i class="fa fa-times-circle pull-right"></i></a>
|
||||||
</h4>
|
</h4>
|
||||||
|
@ -1,16 +1,13 @@
|
|||||||
<div class="form-group">
|
<select ng-if="value.isDropDown()"
|
||||||
<label for="elem-{$ $id $}">{$ value.title() $}</label>
|
id="{$ value.uid() $}" class="form-control"
|
||||||
<select ng-if="value.isDropDown()"
|
|
||||||
id="elem-{$ $id $}" class="form-control"
|
|
||||||
ng-model="value.value" ng-model-options="{getterSetter: true}">
|
ng-model="value.value" ng-model-options="{getterSetter: true}">
|
||||||
<option ng-repeat="option in value.getValues()"
|
<option ng-repeat="option in value.getValues()"
|
||||||
value="{$ option $}"
|
value="{$ option $}"
|
||||||
ng-selected="value.get() == option">{$ value.getLabel(option) $}</option>
|
ng-selected="value.get() == option">{$ value.getLabel(option) $}</option>
|
||||||
</select>
|
</select>
|
||||||
<input ng-if="!value.isDropDown()"
|
<input ng-if="!value.isDropDown()"
|
||||||
type="text" class="form-control" id="elem-{$ $id $}"
|
type="text" class="form-control" id="{$ value.uid() $}"
|
||||||
ng-model="value.value" ng-model-options="{ getterSetter: true }"
|
ng-model="value.value" ng-model-options="{ getterSetter: true }"
|
||||||
validatable-with="value" typeahead-editable="true"
|
validatable-with="value" typeahead-editable="true"
|
||||||
typeahead="option for option in value.getValues() | filter:$viewValue">
|
typeahead="option for option in value.getValues() | filter:$viewValue">
|
||||||
<div ng-show="error" class="alert alert-danger">{$ error $}</div>
|
<div ng-show="error" class="alert alert-danger">{$ error $}</div>
|
||||||
</div>
|
|
||||||
|
@ -1,19 +1,34 @@
|
|||||||
<collapsible-group content="value" on-add="value.add()">
|
<div class="row bottom-xs dictionary">
|
||||||
<div class="three-columns" ng-repeat="subvalue in value.getValues() track by subvalue.keyValue()">
|
<div ng-class="value.inline ? 'col-xs-10' : 'col-xs-12'">
|
||||||
<div class="left-column">
|
<div ng-repeat="(key, field) in value | extractFields track by field.uid()">
|
||||||
<div class="form-group">
|
<div ng-if="field.isAtomic()" class="form-group">
|
||||||
<label for="elem-{$ $id $}.{$ subvalue.uid() $}">
|
<label for="{$ field.uid() $}">
|
||||||
<editable ng-model="subvalue.keyValue" ng-model-options="{getterSetter: true}"></editable>
|
<editable ng-model="field.keyValue" ng-model-options="{getterSetter: true}"></editable>
|
||||||
</label>
|
</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input id="elem-{$ $id $}.{$ subvalue.uid() $}" type="text" class="form-control"
|
<typed-field id="{$ field.uid() $}" value="field" type="{$ field.getType() $}"></typed-field>
|
||||||
ng-model="subvalue.value" ng-model-options="{ getterSetter: true }">
|
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button class="btn btn-default fa fa-minus-circle" type="button"
|
<button class="btn btn-default" ng-click="value.removeItem(field.keyValue())">
|
||||||
ng-click="value.removeItem(subvalue.keyValue())"></button>
|
<i class="fa fa-minus-circle"></i>
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div ng-if="!field.isAtomic()">
|
||||||
|
<collapsible-group ng-if="!field.inline" content="field"
|
||||||
|
class="col-xs-12"
|
||||||
|
title="field.keyValue"
|
||||||
|
on-remove="value.removeItem(field.keyValue())"
|
||||||
|
additive="{$ field.isAdditive() $}" on-add="field.add()">
|
||||||
|
<typed-field value="field" type="{$ field.getType() $}"></typed-field>
|
||||||
|
</collapsible-group>
|
||||||
|
<typed-field ng-if="field.inline"
|
||||||
|
value="field" type="{$ field.getType() $}"></typed-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</collapsible-group>
|
</div>
|
||||||
|
<div ng-if="value.inline" class="col-xs add-entry" style="margin-bottom: 15px">
|
||||||
|
<button class="btn btn-default btn-sm pull-right" ng-click="value.add()">
|
||||||
|
<i class="fa fa-plus"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@ -1,15 +1,24 @@
|
|||||||
<collapsible-group content="value">
|
<div class="frozendict">
|
||||||
<div ng-repeat="row in value | extractRows track by row.id">
|
<div ng-repeat="row in value | extractFields | chunks:2 track by $index">
|
||||||
<div ng-class="{'three-columns': row.index !== undefined}">
|
<div ng-repeat="(key, field) in row track by field.uid()">
|
||||||
<div ng-repeat="item in row | extractItems track by item.uid()"
|
<div ng-if="field.isAtomic()" class="col-xs-6">
|
||||||
ng-class="{'right-column': item.isAtomic() && $odd, 'left-column': item.isAtomic() && $even}">
|
<labeled label="{$ key $}" for="{$ field.uid() $}">
|
||||||
<div class="form-group">
|
<typed-field value="field" type="{$ field.getType() $}"></typed-field>
|
||||||
<label for="elem-{$ $id $}.{$ item.uid() $}">{$ item.title() $}</label>
|
</labeled>
|
||||||
<input type="text" class="form-control" id="elem-{$ $id $}.{$ item.uid() $}" ng-model="item.value"
|
|
||||||
ng-model-options="{getterSetter: true}">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="clearfix" ng-if="$odd"></div>
|
<div ng-if="!field.isAtomic()">
|
||||||
|
<collapsible-group ng-if="!field.inline" class="col-xs-12"
|
||||||
|
content="field" title="key"
|
||||||
|
additive="{$ field.isAdditive() $}" on-add="field.add()">
|
||||||
|
<div ng-class="field.isPlainStructure() ? 'col-xs-6' : 'col-xs-12'">
|
||||||
|
<typed-field value="field" type="{$ field.getType() $}"></typed-field>
|
||||||
|
</div>
|
||||||
|
</collapsible-group>
|
||||||
|
<labeled ng-if="field.inline" class="col-xs-6"
|
||||||
|
label="{$ key $}" for="{$ field.uid() $}">
|
||||||
|
<typed-field value="field" type="{$ field.getType() $}"></typed-field>
|
||||||
|
</labeled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</collapsible-group>
|
</div>
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
<collapsible-group content="value" additive="{$ value.isAdditive() $}"
|
|
||||||
on-add="value.add()"
|
|
||||||
removable="{$ value.isRemovable() $}" on-remove="value.remove()">
|
|
||||||
<div ng-repeat="row in value | extractRows track by row.id">
|
|
||||||
<div ng-class="{'three-columns': row.index !== undefined }">
|
|
||||||
<div ng-repeat="item in row | extractItems track by item.id"
|
|
||||||
ng-class="{'right-column': item.isAtomic() && $odd, 'left-column': item.isAtomic() && $even}">
|
|
||||||
<typed-field value="item" type="{$ item.getType() $}"></typed-field>
|
|
||||||
<div class="clearfix" ng-if="$odd"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</collapsible-group>
|
|
@ -1,9 +1,9 @@
|
|||||||
<collapsible-group content="value" on-add="value.add()">
|
<div class="row bottom-xs list">
|
||||||
<div class="three-columns">
|
<div ng-class="value.inline ? 'col-xs-10' : 'col-xs-12'">
|
||||||
<div class="left-column">
|
<div ng-repeat="(index, field) in value | extractFields track by field.uid()">
|
||||||
<div class="form-group" ng-repeat="subItem in value.getValues() track by $index">
|
<div ng-if="field.isAtomic()" class="form-group">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" ng-model="subItem.value" ng-model-options="{ getterSetter: true }">
|
<typed-field value="field" type="{$ field.getType() $}"></typed-field>
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button class="btn btn-default" ng-click="value.remove($index)">
|
<button class="btn btn-default" ng-click="value.remove($index)">
|
||||||
<i class="fa fa-minus-circle"></i>
|
<i class="fa fa-minus-circle"></i>
|
||||||
@ -11,6 +11,13 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div ng-if="!field.isAtomic()">
|
||||||
|
<typed-field value="field" type="{$ field.getType() $}"></typed-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</collapsible-group>
|
</div>
|
||||||
|
<div ng-if="value.inline" class="col-xs add-btn">
|
||||||
|
<button class="btn btn-default btn-sm pull-right" ng-click="value.add()">
|
||||||
|
<i class="fa fa-plus"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
<div class="form-group">
|
<input type="number" class="form-control" id="{$ value.uid() $}"
|
||||||
<pre>{$ value $}, {$ value.title() $}</pre>
|
|
||||||
<label for="elem-{$ $id $}">{$ value.title() $}</label>
|
|
||||||
<input type="number" class="form-control" id="elem-{$ $id $}"
|
|
||||||
ng-model="value.value" ng-model-options="{ getterSetter: true }"
|
ng-model="value.value" ng-model-options="{ getterSetter: true }"
|
||||||
validatable-with="value">
|
validatable-with="value">
|
||||||
<div ng-show="error" class="alert alert-danger">{$ error $}</div>
|
<div ng-show="error" class="alert alert-danger">{$ error $}</div>
|
||||||
</div>
|
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
<div class="form-group">
|
<input type="text" class="form-control" id="{$ value.uid() $}"
|
||||||
<label for="elem-{$ $id $}">{$ value.title() $}</label>
|
|
||||||
<input type="text" class="form-control" id="elem-{$ $id $}"
|
|
||||||
ng-model="value.value" ng-model-options="{ getterSetter: true }"
|
ng-model="value.value" ng-model-options="{ getterSetter: true }"
|
||||||
validatable-with="value">
|
validatable-with="value">
|
||||||
<div ng-show="error" class="alert alert-danger">{$ error $}</div>
|
<div ng-show="error" class="alert alert-danger">{$ error $}</div>
|
||||||
</div>
|
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
<div class="form-group">
|
<textarea class="form-control" id="{$ value.uid() $}"
|
||||||
<label for="elem-{$ $id $}">{$ value.title() $}</label>
|
|
||||||
<textarea class="form-control" id="elem-{$ $id $}"
|
|
||||||
ng-model="value.value" ng-model-options="{ getterSetter: true }"
|
ng-model="value.value" ng-model-options="{ getterSetter: true }"
|
||||||
validatable-with="value"></textarea>
|
validatable-with="value"></textarea>
|
||||||
<div ng-show="error" class="alert alert-danger">{$ error $}</div>
|
<div ng-show="error" class="alert alert-danger">{$ error $}</div>
|
||||||
</div>
|
|
||||||
|
4
merlin/static/merlin/templates/labeled.html
Normal file
4
merlin/static/merlin/templates/labeled.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<div class="form-group">
|
||||||
|
<label for="{$ for $}">{$ label $}</label>
|
||||||
|
<div ng-transclude></div>
|
||||||
|
</div>
|
@ -66,7 +66,7 @@ describe('merlin directives', function() {
|
|||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
it('shows panel heading when and only when its title() is not false', function() {
|
it('shows panel heading when and only when its title is defined', function() {
|
||||||
var title = 'My Panel',
|
var title = 'My Panel',
|
||||||
element1, element2;
|
element1, element2;
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -47,41 +47,18 @@ describe('merlin models:', function() {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCacheIDs() {
|
|
||||||
return dictObj.getValues().map(function(item) {
|
|
||||||
return item.getID();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('getValues() method', function() {
|
|
||||||
it('caching works from the very beginning', function() {
|
|
||||||
expect(getCacheIDs()).toEqual(['id1', 'id2']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('keyValue() getter/setter can be used from the start', function() {
|
|
||||||
var value = getValueFromCache('id1');
|
|
||||||
|
|
||||||
expect(value.keyValue()).toBe('id1');
|
|
||||||
|
|
||||||
value.keyValue('id3');
|
|
||||||
expect(value.keyValue()).toBe('id3');
|
|
||||||
expect(dictObj.getByID('id3')).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('add() method', function() {
|
describe('add() method', function() {
|
||||||
it('adds an empty value with given key', function() {
|
it('adds an empty value with given key', function() {
|
||||||
dictObj.add('id3');
|
dictObj.add('id3');
|
||||||
|
|
||||||
expect(dictObj.getByID('id3').get()).toBe('');
|
expect(dictObj.getByID('id3').get()).toBe('');
|
||||||
expect(getCacheIDs()).toEqual(['id1', 'id2', 'id3']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keyValue() getter/setter can be used for added values', function() {
|
it('keyValue() getter/setter can be used for added values', function() {
|
||||||
var value;
|
var value;
|
||||||
|
|
||||||
dictObj.add('id3');
|
dictObj.add('id3');
|
||||||
value = getValueFromCache('id3');
|
value = dictObj.getByID('id3');
|
||||||
|
|
||||||
expect(value.keyValue()).toBe('id3');
|
expect(value.keyValue()).toBe('id3');
|
||||||
|
|
||||||
@ -112,31 +89,28 @@ describe('merlin models:', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('empty() method', function() {
|
describe('empty() method', function() {
|
||||||
it('removes all entries in model and in cache', function() {
|
it('removes all entries in model', function() {
|
||||||
dictObj.empty();
|
dictObj.empty();
|
||||||
|
|
||||||
expect(dictObj.getIDs().length).toBe(0);
|
expect(dictObj.getIDs().length).toBe(0);
|
||||||
expect(dictObj.getValues().length).toBe(0);
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('resetKeys() method', function() {
|
describe('resetKeys() method', function() {
|
||||||
it('re-sets dictionary contents to given keys, cache included', function() {
|
it('re-sets dictionary contents to given keys', function() {
|
||||||
dictObj.resetKeys(['key1', 'key2']);
|
dictObj.resetKeys(['key1', 'key2']);
|
||||||
|
|
||||||
expect(dictObj.getIDs()).toEqual(['key1', 'key2']);
|
expect(dictObj.getIDs()).toEqual(['key1', 'key2']);
|
||||||
expect(dictObj.getByID('key1').get()).toBe('');
|
expect(dictObj.getByID('key1').get()).toBe('');
|
||||||
expect(dictObj.getByID('key2').get()).toBe('');
|
expect(dictObj.getByID('key2').get()).toBe('');
|
||||||
expect(getCacheIDs()).toEqual(['key1', 'key2']);
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('removeItem() method', function() {
|
describe('removeItem() method', function() {
|
||||||
it('removes dictionary entry by key from model and cache', function() {
|
it('removes dictionary entry by key from model', function() {
|
||||||
dictObj.removeItem('id1');
|
dictObj.removeItem('id1');
|
||||||
|
|
||||||
expect(dictObj.getByID('id1')).toBeUndefined();
|
expect(dictObj.getByID('id1')).toBeUndefined();
|
||||||
expect(getCacheIDs()).toEqual(['id2']);
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user