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-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,23 +91,23 @@
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
models.yaqllist = fields.list.extend({}, {
|
||||
'*': {'@class': models.YAQLField}
|
||||
});
|
||||
|
||||
models.Action = fields.frozendict.extend({
|
||||
@ -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,9 +102,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- page footer -->
|
||||
<div class="two-panels">
|
||||
<div class="full-width">
|
||||
<div class="pull-right">
|
||||
<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' $}
|
||||
@ -104,5 +112,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -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)();
|
||||
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 {
|
||||
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);
|
||||
return obj.getID();
|
||||
}
|
||||
};
|
||||
} else if (this.$$objParent.instanceof(Barricade.ImmutableObject)) {
|
||||
this.title = context.indexOrKey;
|
||||
}
|
||||
} else {
|
||||
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));
|
||||
});
|
||||
} else {
|
||||
panels.push(Object.create(panelProto).create(items));
|
||||
|
||||
/* 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 {
|
||||
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 {
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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,19 +19,15 @@ editable {
|
||||
}
|
||||
|
||||
.section {
|
||||
.form-group {
|
||||
padding-left: 15px;
|
||||
}
|
||||
.section {
|
||||
margin-left: 15px;
|
||||
h5 {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
a {
|
||||
padding-left: 5px;
|
||||
text-decoration: none;
|
||||
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 {
|
||||
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;
|
||||
|
||||
.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"
|
||||
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="elem-{$ $id $}"
|
||||
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>
|
||||
</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 }">
|
||||
<typed-field id="{$ field.uid() $}" value="field" type="{$ field.getType() $}"></typed-field>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default fa fa-minus-circle" type="button"
|
||||
ng-click="value.removeItem(subvalue.keyValue())"></button>
|
||||
<button class="btn btn-default" ng-click="value.removeItem(field.keyValue())">
|
||||
<i class="fa fa-minus-circle"></i>
|
||||
</button>
|
||||
</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 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>
|
||||
<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>
|
||||
</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="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 }">
|
||||
<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>
|
||||
@ -11,6 +11,13 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="!field.isAtomic()">
|
||||
<typed-field value="field" type="{$ field.getType() $}"></typed-field>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</collapsible-group>
|
||||
|
@ -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 $}"
|
||||
<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>
|
||||
</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 $}"
|
||||
<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>
|
||||
</div>
|
||||
|
@ -1,7 +1,4 @@
|
||||
<div class="form-group">
|
||||
<label for="elem-{$ $id $}">{$ value.title() $}</label>
|
||||
<textarea class="form-control" id="elem-{$ $id $}"
|
||||
<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>
|
||||
</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…
Reference in New Issue
Block a user