Merge "Refactor templates to make them composable"
This commit is contained in:
commit
ec9b892de4
@ -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",
|
||||
|
@ -12,7 +12,7 @@
|
||||
|
||||
function initModule(templates) {
|
||||
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) {
|
||||
var max = Math.max.apply(Math, container.getIDs().map(function(id) {
|
||||
var match = regexp.exec(id);
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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 %}
|
||||
<link href='{{ STATIC_URL }}merlin/scss/merlin.scss' type='text/scss' media='screen' rel='stylesheet' />
|
||||
{% endcompress %}
|
||||
<link href='{{ STATIC_URL }}merlin/libs/flexboxgrid/dist/flexboxgrid.css' type='text/css' media='screen' rel='stylesheet' />
|
||||
{% block merlin-css %}{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<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 }}')">
|
||||
<div class="well">
|
||||
<div class="two-panels">
|
||||
<div class="left-panel">
|
||||
<div class="pull-left">
|
||||
<div class="row">
|
||||
<div class="col-xs row">
|
||||
<div class="col-xs start-xs">
|
||||
<h4><strong>{$ wb.workbook.get('name') $}</strong></h4>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<div class="table-actions clearfix">
|
||||
<div class="col-xs end-xs">
|
||||
<div class="table-actions">
|
||||
<button ng-click="wb.addAction()" class="btn btn-default btn-sm">
|
||||
<span class="fa fa-plus">Add Action</span></button>
|
||||
<button ng-click="wb.addWorkflow()" class="btn btn-default btn-sm">
|
||||
@ -55,8 +56,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-panel">
|
||||
<div class="btn-group btn-toggle pull-right">
|
||||
<div class="col-xs end-xs">
|
||||
<div class="btn-group btn-toggle">
|
||||
<button ng-click="wb.isGraphMode = true" class="btn btn-sm"
|
||||
ng-class="wb.isGraphMode ? 'active btn-primary' : 'btn-default'">Graph</button>
|
||||
<button ng-click="wb.isGraphMode = false" class="btn btn-sm"
|
||||
@ -65,23 +66,31 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- Data panel start -->
|
||||
<div class="two-panels">
|
||||
<div class="left-panel">
|
||||
<panel ng-repeat="panel in wb.workbook | extractPanels track by panel.id"
|
||||
<div class="row">
|
||||
<div class="col-xs">
|
||||
<panel ng-repeat="panel in wb.workbook | extractPanels:wb.keyExtractor track by panel.id"
|
||||
content="panel">
|
||||
<div ng-repeat="row in panel | extractRows track by row.id">
|
||||
<div ng-class="{'two-columns': row.index !== undefined }">
|
||||
<div ng-repeat="item in row | 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 ng-repeat="row in panel | extractFields | chunks:2 track by $index">
|
||||
<div ng-repeat="(label, field) in row track by field.uid()">
|
||||
<div ng-if="field.isAtomic()" class="col-xs-6">
|
||||
<labeled label="{$ label $}" for="{$ field.uid() $}">
|
||||
<typed-field value="field" type="{$ field.getType() $}"></typed-field>
|
||||
</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>
|
||||
</panel>
|
||||
</div>
|
||||
<!-- YAML Panel -->
|
||||
<div class="right-panel">
|
||||
<div class="col-xs">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body" ng-show="!wb.isGraphMode">
|
||||
<pre>{$ wb.workbook.toYAML() $}</pre>
|
||||
@ -93,14 +102,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- page footer -->
|
||||
<div class="two-panels">
|
||||
<div class="full-width">
|
||||
<div class="pull-right">
|
||||
<button ng-click="wb.discardWorkbook()" class="btn btn-default cancel">Cancel</button>
|
||||
<button ng-click="wb.commitWorkbook()" class="btn btn-primary">
|
||||
{$ wb.workbookID ? 'Modify' : 'Create' $}
|
||||
</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs end-xs">
|
||||
<button ng-click="wb.discardWorkbook()" class="btn btn-default cancel">Cancel</button>
|
||||
<button ng-click="wb.commitWorkbook()" class="btn btn-primary">
|
||||
{$ wb.workbookID ? 'Modify' : 'Create' $}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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() {
|
||||
|
@ -39,9 +39,21 @@
|
||||
* retrieves a template by its name which is the same as model's type and renders it,
|
||||
* 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() {
|
||||
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));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -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});
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
|
@ -19,7 +19,7 @@
|
||||
function fieldTemplates() {
|
||||
return [
|
||||
'dictionary', 'frozendict', 'list',
|
||||
'string', 'text', 'group', 'number', 'choices'
|
||||
'string', 'text', 'number', 'choices'
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,18 +1,18 @@
|
||||
<div class="section">
|
||||
<div class="section-heading three-columns">
|
||||
<div class="both-columns">
|
||||
<div class="section-heading row">
|
||||
<div class="col-xs-10">
|
||||
<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>
|
||||
<editable ng-if="removable" ng-model="group.title"
|
||||
<editable ng-if="editable" ng-model="title"
|
||||
ng-model-options="{getterSetter: true}"></editable>
|
||||
<span ng-if="!removable">{$ group.title() $}</span>
|
||||
<span ng-if="!editable">{$ ::title $}</span>
|
||||
</h5>
|
||||
</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()">
|
||||
<i class="fa fa-plus"></i></button>
|
||||
</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()">
|
||||
<i class="fa fa-times-circle pull-right"></i></a>
|
||||
</div>
|
||||
|
@ -1,9 +1,11 @@
|
||||
<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">
|
||||
<a ng-click="isCollapsed = !isCollapsed" href="">
|
||||
<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()">
|
||||
<i class="fa fa-times-circle pull-right"></i></a>
|
||||
</h4>
|
||||
|
@ -1,16 +1,13 @@
|
||||
<div class="form-group">
|
||||
<label for="elem-{$ $id $}">{$ value.title() $}</label>
|
||||
<select ng-if="value.isDropDown()"
|
||||
id="elem-{$ $id $}" class="form-control"
|
||||
ng-model="value.value" ng-model-options="{getterSetter: true}">
|
||||
<option ng-repeat="option in value.getValues()"
|
||||
value="{$ option $}"
|
||||
ng-selected="value.get() == option">{$ value.getLabel(option) $}</option>
|
||||
</select>
|
||||
<input ng-if="!value.isDropDown()"
|
||||
type="text" class="form-control" id="elem-{$ $id $}"
|
||||
ng-model="value.value" ng-model-options="{ getterSetter: true }"
|
||||
validatable-with="value" typeahead-editable="true"
|
||||
typeahead="option for option in value.getValues() | filter:$viewValue">
|
||||
<div ng-show="error" class="alert alert-danger">{$ error $}</div>
|
||||
</div>
|
||||
<select ng-if="value.isDropDown()"
|
||||
id="{$ value.uid() $}" class="form-control"
|
||||
ng-model="value.value" ng-model-options="{getterSetter: true}">
|
||||
<option ng-repeat="option in value.getValues()"
|
||||
value="{$ option $}"
|
||||
ng-selected="value.get() == option">{$ value.getLabel(option) $}</option>
|
||||
</select>
|
||||
<input ng-if="!value.isDropDown()"
|
||||
type="text" class="form-control" id="{$ value.uid() $}"
|
||||
ng-model="value.value" ng-model-options="{ getterSetter: true }"
|
||||
validatable-with="value" typeahead-editable="true"
|
||||
typeahead="option for option in value.getValues() | filter:$viewValue">
|
||||
<div ng-show="error" class="alert alert-danger">{$ error $}</div>
|
||||
|
@ -1,19 +1,34 @@
|
||||
<collapsible-group content="value" on-add="value.add()">
|
||||
<div class="three-columns" ng-repeat="subvalue in value.getValues() track by subvalue.keyValue()">
|
||||
<div class="left-column">
|
||||
<div class="form-group">
|
||||
<label for="elem-{$ $id $}.{$ subvalue.uid() $}">
|
||||
<editable ng-model="subvalue.keyValue" ng-model-options="{getterSetter: true}"></editable>
|
||||
<div class="row bottom-xs dictionary">
|
||||
<div ng-class="value.inline ? 'col-xs-10' : 'col-xs-12'">
|
||||
<div ng-repeat="(key, field) in value | extractFields track by field.uid()">
|
||||
<div ng-if="field.isAtomic()" class="form-group">
|
||||
<label for="{$ field.uid() $}">
|
||||
<editable ng-model="field.keyValue" ng-model-options="{getterSetter: true}"></editable>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input id="elem-{$ $id $}.{$ subvalue.uid() $}" type="text" class="form-control"
|
||||
ng-model="subvalue.value" ng-model-options="{ getterSetter: true }">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default fa fa-minus-circle" type="button"
|
||||
ng-click="value.removeItem(subvalue.keyValue())"></button>
|
||||
</span>
|
||||
<typed-field id="{$ field.uid() $}" value="field" type="{$ field.getType() $}"></typed-field>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" ng-click="value.removeItem(field.keyValue())">
|
||||
<i class="fa fa-minus-circle"></i>
|
||||
</button>
|
||||
</span>
|
||||
</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>
|
||||
</collapsible-group>
|
||||
<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 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.uid()"
|
||||
ng-class="{'right-column': item.isAtomic() && $odd, 'left-column': item.isAtomic() && $even}">
|
||||
<div class="form-group">
|
||||
<label for="elem-{$ $id $}.{$ item.uid() $}">{$ item.title() $}</label>
|
||||
<input type="text" class="form-control" id="elem-{$ $id $}.{$ item.uid() $}" ng-model="item.value"
|
||||
ng-model-options="{getterSetter: true}">
|
||||
</div>
|
||||
<div class="clearfix" ng-if="$odd"></div>
|
||||
<div class="frozendict">
|
||||
<div ng-repeat="row in value | extractFields | chunks:2 track by $index">
|
||||
<div ng-repeat="(key, field) in row track by field.uid()">
|
||||
<div ng-if="field.isAtomic()" class="col-xs-6">
|
||||
<labeled label="{$ key $}" for="{$ field.uid() $}">
|
||||
<typed-field value="field" type="{$ field.getType() $}"></typed-field>
|
||||
</labeled>
|
||||
</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>
|
||||
</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,16 +1,23 @@
|
||||
<collapsible-group content="value" on-add="value.add()">
|
||||
<div class="three-columns">
|
||||
<div class="left-column">
|
||||
<div class="form-group" ng-repeat="subItem in value.getValues() track by $index">
|
||||
<div class="row bottom-xs list">
|
||||
<div ng-class="value.inline ? 'col-xs-10' : 'col-xs-12'">
|
||||
<div ng-repeat="(index, field) in value | extractFields track by field.uid()">
|
||||
<div ng-if="field.isAtomic()" class="form-group">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" ng-model="subItem.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>
|
||||
<typed-field value="field" type="{$ field.getType() $}"></typed-field>
|
||||
<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 ng-if="!field.isAtomic()">
|
||||
<typed-field value="field" type="{$ field.getType() $}"></typed-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</collapsible-group>
|
||||
<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">
|
||||
<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 }"
|
||||
validatable-with="value">
|
||||
<div ng-show="error" class="alert alert-danger">{$ error $}</div>
|
||||
</div>
|
||||
<input type="number" class="form-control" id="{$ value.uid() $}"
|
||||
ng-model="value.value" ng-model-options="{ getterSetter: true }"
|
||||
validatable-with="value">
|
||||
<div ng-show="error" class="alert alert-danger">{$ error $}</div>
|
||||
|
@ -1,7 +1,4 @@
|
||||
<div class="form-group">
|
||||
<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 }"
|
||||
validatable-with="value">
|
||||
<div ng-show="error" class="alert alert-danger">{$ error $}</div>
|
||||
</div>
|
||||
<input type="text" class="form-control" id="{$ value.uid() $}"
|
||||
ng-model="value.value" ng-model-options="{ getterSetter: true }"
|
||||
validatable-with="value">
|
||||
<div ng-show="error" class="alert alert-danger">{$ error $}</div>
|
||||
|
@ -1,7 +1,4 @@
|
||||
<div class="form-group">
|
||||
<label for="elem-{$ $id $}">{$ value.title() $}</label>
|
||||
<textarea class="form-control" id="elem-{$ $id $}"
|
||||
ng-model="value.value" ng-model-options="{ getterSetter: true }"
|
||||
validatable-with="value"></textarea>
|
||||
<div ng-show="error" class="alert alert-danger">{$ error $}</div>
|
||||
</div>
|
||||
<textarea class="form-control" id="{$ value.uid() $}"
|
||||
ng-model="value.value" ng-model-options="{ getterSetter: true }"
|
||||
validatable-with="value"></textarea>
|
||||
<div ng-show="error" class="alert alert-danger">{$ error $}</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;
|
||||
}
|
||||
|
||||
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',
|
||||
element1, element2;
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -47,41 +47,18 @@ describe('merlin models:', function() {
|
||||
return value;
|
||||
}
|
||||
|
||||
function getCacheIDs() {
|
||||
return dictObj.getValues().map(function(item) {
|
||||
return item.getID();
|
||||
});
|
||||
}
|
||||
|
||||
describe('getValues() method', function() {
|
||||
it('caching works from the very beginning', function() {
|
||||
expect(getCacheIDs()).toEqual(['id1', 'id2']);
|
||||
});
|
||||
|
||||
it('keyValue() getter/setter can be used from the start', function() {
|
||||
var value = getValueFromCache('id1');
|
||||
|
||||
expect(value.keyValue()).toBe('id1');
|
||||
|
||||
value.keyValue('id3');
|
||||
expect(value.keyValue()).toBe('id3');
|
||||
expect(dictObj.getByID('id3')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('add() method', function() {
|
||||
it('adds an empty value with given key', function() {
|
||||
dictObj.add('id3');
|
||||
|
||||
expect(dictObj.getByID('id3').get()).toBe('');
|
||||
expect(getCacheIDs()).toEqual(['id1', 'id2', 'id3']);
|
||||
});
|
||||
|
||||
it('keyValue() getter/setter can be used for added values', function() {
|
||||
var value;
|
||||
|
||||
dictObj.add('id3');
|
||||
value = getValueFromCache('id3');
|
||||
value = dictObj.getByID('id3');
|
||||
|
||||
expect(value.keyValue()).toBe('id3');
|
||||
|
||||
@ -112,31 +89,28 @@ describe('merlin models:', function() {
|
||||
});
|
||||
|
||||
describe('empty() method', function() {
|
||||
it('removes all entries in model and in cache', function() {
|
||||
it('removes all entries in model', function() {
|
||||
dictObj.empty();
|
||||
|
||||
expect(dictObj.getIDs().length).toBe(0);
|
||||
expect(dictObj.getValues().length).toBe(0);
|
||||
})
|
||||
});
|
||||
|
||||
describe('resetKeys() method', function() {
|
||||
it('re-sets dictionary contents to given keys, cache included', function() {
|
||||
it('re-sets dictionary contents to given keys', function() {
|
||||
dictObj.resetKeys(['key1', 'key2']);
|
||||
|
||||
expect(dictObj.getIDs()).toEqual(['key1', 'key2']);
|
||||
expect(dictObj.getByID('key1').get()).toBe('');
|
||||
expect(dictObj.getByID('key2').get()).toBe('');
|
||||
expect(getCacheIDs()).toEqual(['key1', 'key2']);
|
||||
})
|
||||
});
|
||||
|
||||
describe('removeItem() method', function() {
|
||||
it('removes dictionary entry by key from model and cache', function() {
|
||||
it('removes dictionary entry by key from model', function() {
|
||||
dictObj.removeItem('id1');
|
||||
|
||||
expect(dictObj.getByID('id1')).toBeUndefined();
|
||||
expect(getCacheIDs()).toEqual(['id2']);
|
||||
})
|
||||
});
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user