Fix changing Action/Workflow/Task id

To make it happen <collapsible-group> directive had to undergo the
same transformation that was applied to <panel> directive: instead of
passing just a '@title' reference, the whole '=content' reference is
now passed to the <collapsible-group>'s scope. This allows to use
<editable> directive inside it with 'ng-model=group.title' as
getter/setter method. Yet <collapsible-group>'s scope wasn't remade as
radically as <panel>'s was - '&on-add' and '&on-remove' are still
there and left for future refactoring.

Change-Id: I4de7a542f282efee6deb34d4957a5873d617ad64
Closes-Bug: #1446171
Closes-Bug: #1446226
This commit is contained in:
Timur Sufiev 2015-04-28 17:45:07 +03:00
parent f219a63e21
commit 531dc56c64
21 changed files with 172 additions and 102 deletions

View File

@ -5,10 +5,12 @@
'use strict'; 'use strict';
angular.module('mistral') angular.module('mistral')
.value('baseActionID', 'action')
.value('baseWorkflowID', 'workflow')
.controller('workbookCtrl', .controller('workbookCtrl',
['$scope', 'mistral.workbook.models', function($scope, models) { ['$scope', 'mistral.workbook.models', 'baseActionID', 'baseWorkflowID',
var workbook = models.Workbook.create({name: 'My Workbook'}); function($scope, models, baseActionId, baseWorkflowId) {
$scope.workbook = workbook; $scope.workbook = models.Workbook.create({name: 'My Workbook'});
function getNextIDSuffix(container, regexp) { function getNextIDSuffix(container, regexp) {
var max = Math.max.apply(Math, container.getIDs().map(function(id) { var max = Math.max.apply(Math, container.getIDs().map(function(id) {
@ -28,8 +30,6 @@
return getNextIDSuffix(container, regexp); return getNextIDSuffix(container, regexp);
} }
var baseActionId = 'action', baseWorkflowId = 'workflow';
$scope.addAction = function() { $scope.addAction = function() {
var nextSuffix = getWorkbookNextIDSuffix(baseActionId), var nextSuffix = getWorkbookNextIDSuffix(baseActionId),
newID = baseActionId + nextSuffix; newID = baseActionId + nextSuffix;

View File

@ -119,21 +119,8 @@
} }
}); });
return self; return self;
},
_getPrettyJSON: function() {
var json = fields.frozendict._getPrettyJSON.apply(this, arguments);
delete json.name;
return json;
} }
}, { }, {
'name': {
'@class': fields.string.extend({}, {
'@meta': {
'index': 0,
'row': 0
}
})
},
'base': { 'base': {
'@class': fields.string.extend({ '@class': fields.string.extend({
create: function(json, parameters) { create: function(json, parameters) {
@ -209,11 +196,6 @@
remove: function() { remove: function() {
this.emit('change', 'taskRemove', this.getID()); this.emit('change', 'taskRemove', this.getID());
}, },
_getPrettyJSON: function() {
var json = fields.frozendict._getPrettyJSON.apply(this, arguments);
delete json.name;
return json;
}
}, { }, {
'@meta': { '@meta': {
'baseKey': 'task', 'baseKey': 'task',
@ -222,14 +204,6 @@
'additive': false, 'additive': false,
'removable': true 'removable': true
}, },
'name': {
'@class': fields.string.extend({}, {
'@meta': {
'index': 0,
'row': 0
}
})
},
'type': { 'type': {
'@class': fields.string.extend({}, { '@class': fields.string.extend({}, {
'@enum': [{ '@enum': [{
@ -239,7 +213,7 @@
}], }],
'@default': 'action', '@default': 'action',
'@meta': { '@meta': {
'index': 1, 'index': 0,
'row': 0 'row': 0
} }
}) })
@ -247,7 +221,7 @@
'description': { 'description': {
'@class': fields.text.extend({}, { '@class': fields.text.extend({}, {
'@meta': { '@meta': {
'index': 1, 'index': 2,
'row': 1 'row': 1
} }
}) })
@ -403,8 +377,8 @@
'action': { 'action': {
'@class': fields.string.extend({}, { '@class': fields.string.extend({}, {
'@meta': { '@meta': {
'row': 1, 'row': 0,
'index': 2 'index': 1
} }
}) })
} }
@ -416,8 +390,8 @@
'workflow': { 'workflow': {
'@class': fields.string.extend({}, { '@class': fields.string.extend({}, {
'@meta': { '@meta': {
'row': 1, 'row': 0,
'index': 2 'index': 1
} }
}) })
} }
@ -448,21 +422,8 @@
} }
}); });
return self; return self;
},
_getPrettyJSON: function() {
var json = fields.frozendict._getPrettyJSON.apply(this, arguments);
delete json.name;
return json;
} }
}, { }, {
'name': {
'@class': fields.string.extend({}, {
'@meta': {
'index': 0,
'row': 0
}
})
},
'type': { 'type': {
'@class': fields.string.extend({}, { '@class': fields.string.extend({}, {
'@enum': ['reverse', 'direct'], '@enum': ['reverse', 'direct'],

View File

@ -1,4 +1,4 @@
<collapsible-group title="{$ title $}" <collapsible-group content="value"
on-add="value.add()"> on-add="value.add()">
<div class="three-columns" ng-repeat="subItem in value.getValues() track by $index" <div class="three-columns" ng-repeat="subItem in value.getValues() track by $index"
ng-class="subItem.get('type').get()"> ng-class="subItem.get('type').get()">

View File

@ -1,7 +1,6 @@
<collapsible-group title="{$ title $}" on-add="value.add()"> <collapsible-group content="value" on-add="value.add()">
<div class="three-columns" <div class="three-columns"
ng-repeat="subItem in value.getValues() track by $index"> ng-repeat="subItem in value.getValues() track by $index">
<pre>{$ subItem $}</pre>
<div class="left-column" ng-show="subItem.showYaql"> <div class="left-column" ng-show="subItem.showYaql">
<div class="form-group"> <div class="form-group">
<textarea class="form-control" ng-model="subItem.get('yaql').value" <textarea class="form-control" ng-model="subItem.get('yaql').value"

View File

@ -68,7 +68,7 @@
<div ng-class="{'two-columns': row.index !== undefined }"> <div ng-class="{'two-columns': row.index !== undefined }">
<div ng-repeat="item in row | extractItems track by item.id" <div ng-repeat="item in row | extractItems track by item.id"
ng-class="{'right-column': item.isAtomic() && $odd, 'left-column': item.isAtomic() && $even}"> ng-class="{'right-column': item.isAtomic() && $odd, 'left-column': item.isAtomic() && $even}">
<typed-field title="{$ item.getTitle() $}" value="item" type="{$ item.getType() $}"></typed-field> <typed-field value="item" type="{$ item.getType() $}"></typed-field>
<div class="clearfix" ng-if="$odd"></div> <div class="clearfix" ng-if="$odd"></div>
</div> </div>
</div> </div>

View File

@ -74,15 +74,37 @@ describe('workbook model logic', function() {
workbook.get('workflows').push({name: 'Workflow 1'}, {id: workflowID}); workbook.get('workflows').push({name: 'Workflow 1'}, {id: workflowID});
}); });
it('a task deletion works in conjunction with tasks logic', function() { describe('', function() {
var workflow = getWorkflow(workflowID), beforeEach(function() {
params = utils.extend(workflow._parameters, {id: taskID}); var workflow = getWorkflow(workflowID),
params = workflow._parameters;
workflow.get('tasks').push({name: 'Task 1'}, utils.extend(params, {id: taskID}));
});
workflow.get('tasks').push({name: 'Task 1'}, params); it("corresponding JSON has the right key for the Task", function() {
expect(getTask(taskID)).toBeDefined(); var json = workbook.toJSON({pretty: true});
expect(json.workflows[workflowID].tasks[taskID]).toBeDefined();
});
it("once the task ID is changed, it's reflected in JSON", function() {
var newID = 'task10',
json;
getTask(taskID).setID(newID);
json = workbook.toJSON({pretty: true});
expect(json.workflows[workflowID].tasks[taskID]).toBeUndefined();
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();
});
getTask(taskID).remove();
expect(getTask(taskID)).toBeUndefined();
}); });
describe("which start with the 'direct' workflow:", function() { describe("which start with the 'direct' workflow:", function() {
@ -200,15 +222,39 @@ describe('workbook model logic', function() {
it('creates action with predefined name', function() { it('creates action with predefined name', function() {
$scope.addAction(); $scope.addAction();
expect(workbook.get('actions').get(0).get('name').get()).toBeGreaterThan(''); expect(workbook.get('actions').get(0).getID()).toBeGreaterThan('');
});
describe('', function() {
var actionID;
beforeEach(inject(function(baseActionID) {
actionID = baseActionID + '1';
}));
it("corresponding JSON has the right key for the Action", function() {
$scope.addAction();
expect(workbook.toJSON({pretty: true}).actions[actionID]).toBeDefined();
});
it("once the Action ID is changed, it's reflected in JSON", function() {
var newID = 'action10';
$scope.addAction();
workbook.get('actions').getByID(actionID).setID(newID);
expect(workbook.toJSON({pretty: true}).actions[actionID]).toBeUndefined();
expect(workbook.toJSON({pretty: true}).actions[newID]).toBeDefined();
});
}); });
it('creates actions with different names on 2 successive calls', function() { it('creates actions with different names on 2 successive calls', function() {
$scope.addAction(); $scope.addAction();
$scope.addAction(); $scope.addAction();
expect(workbook.get('actions').get(0).get('name').get()).not.toEqual( expect(workbook.get('actions').get(0).getID()).not.toEqual(
workbook.get('actions').get(1).get('name').get()) workbook.get('actions').get(1).getID())
}); });
}); });
@ -219,21 +265,45 @@ describe('workbook model logic', function() {
expect(workbook.get('workflows').get(0)).toBeDefined(); expect(workbook.get('workflows').get(0)).toBeDefined();
}); });
describe('', function() {
var workflowID;
beforeEach(inject(function(baseWorkflowID) {
workflowID = baseWorkflowID + '1';
}));
it("corresponding JSON has the right key for the Workflow", function() {
$scope.addWorkflow();
expect(workbook.toJSON({pretty: true}).workflows[workflowID]).toBeDefined();
});
it("once the workflow ID is changed, it's reflected in JSON", function() {
var newID = 'workflow10';
$scope.addWorkflow();
workbook.get('workflows').getByID(workflowID).setID(newID);
expect(workbook.toJSON({pretty: true}).workflows[workflowID]).toBeUndefined();
expect(workbook.toJSON({pretty: true}).workflows[newID]).toBeDefined();
});
});
it('creates workflow with predefined name', function() { it('creates workflow with predefined name', function() {
$scope.addWorkflow(); $scope.addWorkflow();
expect(workbook.get('workflows').get(0).get('name').get()).toBeGreaterThan(''); expect(workbook.get('workflows').get(0).getID()).toBeGreaterThan('');
}); });
it('creates workflows with different names on 2 successive calls', function() { it('creates workflows with different names on 2 successive calls', function() {
$scope.addWorkflow(); $scope.addWorkflow();
$scope.addWorkflow(); $scope.addWorkflow();
expect(workbook.get('workflows').get(0).get('name').get()).not.toEqual( expect(workbook.get('workflows').get(0).getID()).not.toEqual(
workbook.get('workflows').get(1).get('name').get()) workbook.get('workflows').get(1).getID())
}); });
}); });
}) })
}); });

View File

@ -96,7 +96,7 @@
templateUrl: '/static/merlin/templates/collapsible-group.html', templateUrl: '/static/merlin/templates/collapsible-group.html',
transclude: true, transclude: true,
scope: { scope: {
title: '@', group: '=content',
onAdd: '&', onAdd: '&',
onRemove: '&' onRemove: '&'
}, },
@ -117,7 +117,6 @@
return { return {
restrict: 'E', restrict: 'E',
scope: { scope: {
title: '@',
value: '=', value: '=',
type: '@' type: '@'
}, },

View File

@ -52,7 +52,7 @@
this.isAtomic = function() { this.isAtomic = function() {
return ['number', 'string', 'text', 'choices'].indexOf(this.getType()) > -1; return ['number', 'string', 'text', 'choices'].indexOf(this.getType()) > -1;
}; };
this.getTitle = function() { this.title = function() {
var title = utils.getMeta(this, 'title'); var title = utils.getMeta(this, 'title');
if ( !title ) { if ( !title ) {
if ( this.instanceof(Barricade.ImmutableObject) ) { if ( this.instanceof(Barricade.ImmutableObject) ) {

View File

@ -37,13 +37,14 @@
return this; return this;
}, },
title: function() { title: function() {
var entity; var newID;
if ( this._barricadeContainer ) { if ( this._barricadeContainer ) {
entity = this._barricadeContainer.getByID(this._barricadeId).get('name');
if ( arguments.length ) { if ( arguments.length ) {
entity.set(arguments[0]); newID = arguments[0];
this._barricadeContainer.getByID(this._barricadeId).setID(newID);
this._barricadeId = newID;
} else { } else {
return entity.get(); return this._barricadeId;
} }
} }
}, },

View File

@ -26,6 +26,16 @@
return removable; return removable;
}; };
if ( removable ) { // conditionally override common .title()
self.title = function() {
if ( arguments.length ) {
self.setID(arguments[0]);
} else {
return self.getID();
}
}
}
self.setType('group'); self.setType('group');
return self; return self;

View File

@ -47,8 +47,10 @@
} }
function getMeta(item, key) { function getMeta(item, key) {
var meta = item._schema['@meta']; if ( item ) {
return meta && meta[key]; var meta = item._schema['@meta'];
return meta && meta[key];
}
} }
function makeTitle(str) { function makeTitle(str) {

View File

@ -3,13 +3,16 @@
<div class="both-columns"> <div class="both-columns">
<h5><a ng-click="isCollapsed = !isCollapsed" class="collapse-entries" href="#"> <h5><a ng-click="isCollapsed = !isCollapsed" class="collapse-entries" href="#">
<i class="fa" ng-class="isCollapsed ? 'fa-plus-square-o' : 'fa-minus-square-o'"></i></a> <i class="fa" ng-class="isCollapsed ? 'fa-plus-square-o' : 'fa-minus-square-o'"></i></a>
{$ title $}</h5> <editable ng-if="removable" ng-model="group.title"
ng-model-options="{getterSetter: true}"></editable>
<span ng-if="!removable">{$ group.title() $}</span>
</h5>
</div> </div>
<div ng-show="additive" class="add-btn button-column add-entry"> <div ng-if="additive" class="add-btn button-column add-entry">
<button class="btn btn-default btn-sm pull-right" ng-click="onAdd()"> <button class="btn btn-default btn-sm pull-right" ng-click="onAdd()">
<i class="fa fa-plus"></i></button> <i class="fa fa-plus"></i></button>
</div> </div>
<div ng-show="removable" class="add-btn button-column remove-entry"> <div ng-if="removable" class="add-btn button-column remove-entry">
<a href="#" ng-click="onRemove()"> <a href="#" ng-click="onRemove()">
<i class="fa fa-times-circle pull-right"></i></a> <i class="fa fa-times-circle pull-right"></i></a>
</div> </div>

View File

@ -1,5 +1,5 @@
<div class="form-group"> <div class="form-group">
<label for="elem-{$ $id $}.$index">{$ title $}</label> <label for="elem-{$ $id $}.$index">{$ value.title() $}</label>
<select id="elem-{$ $id $}.$index" class="form-control" <select id="elem-{$ $id $}.$index" class="form-control"
ng-model="value.value" ng-model-options="{getterSetter: true}"> ng-model="value.value" ng-model-options="{getterSetter: true}">
<option ng-repeat="option in value.getValues()" <option ng-repeat="option in value.getValues()"

View File

@ -1,4 +1,4 @@
<collapsible-group title="{$ title $}" on-add="value.add()"> <collapsible-group content="value" on-add="value.add()">
<div class="three-columns" ng-repeat="(key, subvalue) in value.getValues() track by key"> <div class="three-columns" ng-repeat="(key, subvalue) in value.getValues() track by key">
<div class="left-column"> <div class="left-column">
<div class="form-group"> <div class="form-group">

View File

@ -1,10 +1,10 @@
<collapsible-group title="{$ title $}"> <collapsible-group content="value">
<div ng-repeat="row in value | extractRows track by row.id"> <div ng-repeat="row in value | extractRows track by row.id">
<div ng-class="{'three-columns': row.index !== undefined}"> <div ng-class="{'three-columns': row.index !== undefined}">
<div ng-repeat="item in row | extractItems track by item.id" <div ng-repeat="item in row | extractItems track by item.id"
ng-class="{'right-column': item.isAtomic() && $odd, 'left-column': item.isAtomic() && $even}"> ng-class="{'right-column': item.isAtomic() && $odd, 'left-column': item.isAtomic() && $even}">
<div class="form-group"> <div class="form-group">
<label for="elem-{$ $id $}.{$ item.getID() $}">{$ item.getTitle() $}</label> <label for="elem-{$ $id $}.{$ item.getID() $}">{$ item.title() $}</label>
<input type="text" class="form-control" id="elem-{$ $id $}.{$ item.getID() $}" ng-model="item.value" <input type="text" class="form-control" id="elem-{$ $id $}.{$ item.getID() $}" ng-model="item.value"
ng-model-options="{getterSetter: true}"> ng-model-options="{getterSetter: true}">
</div> </div>

View File

@ -1,11 +1,11 @@
<collapsible-group <collapsible-group content="value" additive="{$ value.isAdditive() $}"
title="{$ title $}" additive="{$ value.isAdditive() $}" on-add="value.add()" on-add="value.add()"
removable="{$ value.isRemovable() $}" on-remove="value.remove()"> removable="{$ value.isRemovable() $}" on-remove="value.remove()">
<div ng-repeat="row in value | extractRows track by row.id"> <div ng-repeat="row in value | extractRows track by row.id">
<div ng-class="{'three-columns': row.index !== undefined }"> <div ng-class="{'three-columns': row.index !== undefined }">
<div ng-repeat="item in row | extractItems track by item.id" <div ng-repeat="item in row | extractItems track by item.id"
ng-class="{'right-column': item.isAtomic() && $odd, 'left-column': item.isAtomic() && $even}"> ng-class="{'right-column': item.isAtomic() && $odd, 'left-column': item.isAtomic() && $even}">
<typed-field title="{$ item.getTitle() $}" value="item" type="{$ item.getType() $}"></typed-field> <typed-field value="item" type="{$ item.getType() $}"></typed-field>
<div class="clearfix" ng-if="$odd"></div> <div class="clearfix" ng-if="$odd"></div>
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
<collapsible-group title="{$ title $}" on-add="value.add()"> <collapsible-group content="value" on-add="value.add()">
<div class="three-columns"> <div class="three-columns">
<div class="left-column"> <div class="left-column">
<div class="form-group" ng-repeat="subItem in value.getValues() track by $index"> <div class="form-group" ng-repeat="subItem in value.getValues() track by $index">

View File

@ -1,5 +1,6 @@
<div class="form-group"> <div class="form-group">
<label for="elem-{$ $id $}">{$ title $}</label> <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" <input type="number" class="form-control" id="elem-{$ $id $}" ng-model="value.value"
ng-model-options="{ getterSetter: true }"> ng-model-options="{ getterSetter: true }">
</div> </div>

View File

@ -1,5 +1,5 @@
<div class="form-group"> <div class="form-group">
<label for="elem-{$ $id $}">{$ title $}</label> <label for="elem-{$ $id $}">{$ value.title() $}</label>
<input ng-if="!value.getSuggestions" <input ng-if="!value.getSuggestions"
type="text" class="form-control" id="elem-{$ $id $}" ng-model="value.value" type="text" class="form-control" id="elem-{$ $id $}" ng-model="value.value"
ng-model-options="{ getterSetter: true }"> ng-model-options="{ getterSetter: true }">

View File

@ -1,5 +1,5 @@
<div class="form-group"> <div class="form-group">
<label for="elem-{$ $id $}">{$ title $}</label> <label for="elem-{$ $id $}">{$ value.title() $}</label>
<textarea class="form-control" id="elem-{$ $id $}" ng-model="value.value" <textarea class="form-control" id="elem-{$ $id $}" ng-model="value.value"
ng-model-options="{ getterSetter: true }"></textarea> ng-model-options="{ getterSetter: true }"></textarea>
</div> </div>

View File

@ -137,17 +137,15 @@ describe('merlin directives', function() {
} }
function getGroupRemoveBtn(groupElem) { function getGroupRemoveBtn(groupElem) {
var div = groupElem.children().children().eq(0).children().eq(2); return groupElem.find('.remove-entry');
return div.hasClass('remove-entry') && div;
} }
function getGroupAddBtn(groupElem) { function getGroupAddBtn(groupElem) {
var div = groupElem.children().children().eq(0).children().eq(1); return groupElem.find('.add-entry');
return div.hasClass('add-entry') && div;
} }
function getCollapseBtn(groupElem) { function getCollapseBtn(groupElem) {
return groupElem.children().children().eq(0).children().eq(0).find('a'); return groupElem.find('.collapse-entries');
} }
function makeGroupElement(contents) { function makeGroupElement(contents) {
@ -193,7 +191,7 @@ describe('merlin directives', function() {
element1 = makeGroupElement(''); element1 = makeGroupElement('');
element2 = makeGroupElement('on-remove="remove()"'); element2 = makeGroupElement('on-remove="remove()"');
expect(getGroupRemoveBtn(element1).hasClass('ng-hide')).toBe(true); expect(getGroupRemoveBtn(element1).length).toBe(0);
expect(getGroupRemoveBtn(element2).hasClass('ng-hide')).toBe(false); expect(getGroupRemoveBtn(element2).hasClass('ng-hide')).toBe(false);
}); });
@ -202,7 +200,7 @@ describe('merlin directives', function() {
$scope.remove = function() {}; $scope.remove = function() {};
element = makeGroupElement('on-remove="remove()" removable="false"'); element = makeGroupElement('on-remove="remove()" removable="false"');
expect(getGroupRemoveBtn(element).hasClass('ng-hide')).toBe(true); expect(getGroupRemoveBtn(element).length).toBe(0);
}); });
it('requires to specify `on-add` to make group additive', function() { it('requires to specify `on-add` to make group additive', function() {
@ -211,7 +209,7 @@ describe('merlin directives', function() {
element1 = makeGroupElement(''); element1 = makeGroupElement('');
element2 = makeGroupElement('on-add="add()"'); element2 = makeGroupElement('on-add="add()"');
expect(getGroupAddBtn(element1).hasClass('ng-hide')).toBe(true); expect(getGroupAddBtn(element1).length).toBe(0);
expect(getGroupAddBtn(element2).hasClass('ng-hide')).toBe(false); expect(getGroupAddBtn(element2).hasClass('ng-hide')).toBe(false);
}); });
@ -220,7 +218,7 @@ describe('merlin directives', function() {
$scope.add = function() {}; $scope.add = function() {};
element = makeGroupElement('on-add="add()" additive="false"'); element = makeGroupElement('on-add="add()" additive="false"');
expect(getGroupAddBtn(element).hasClass('ng-hide')).toBe(true); expect(getGroupAddBtn(element).length).toBe(0);
}); });
it('contents are inserted into div.collapse tag', function() { it('contents are inserted into div.collapse tag', function() {
@ -258,6 +256,32 @@ describe('merlin directives', function() {
$httpBackend.flush(); $httpBackend.flush();
expect(element.html()).toContain('<textarea'); expect(element.html()).toContain('<textarea');
});
describe('various types', function() {
describe('.title() of every field except group', function() {
it("tries to extract title from '@meta' key", function() {
});
it("when no title found in '@meta', takes value of 'name' subfield given it's ImmutableObj", function() {
});
it("when no title found both in '@meta' and in 'name' subfield, uses capitalized field ID", function() {
});
});
describe('.title() of group field', function() {
it('if the field is not removable, uses the conventional .title()', function() {
});
it('if the field is removable, uses .title() as a wrapper around .getID()/.setID()', function() {
});
})
}) })
}); });
@ -323,4 +347,4 @@ describe('merlin directives', function() {
}); });
}); });
}); });