diff --git a/extensions/mistral/static/mistral/js/mistral.workbook.controllers.js b/extensions/mistral/static/mistral/js/mistral.workbook.controllers.js index 0271ea4..14c1c67 100644 --- a/extensions/mistral/static/mistral/js/mistral.workbook.controllers.js +++ b/extensions/mistral/static/mistral/js/mistral.workbook.controllers.js @@ -21,7 +21,7 @@ function getWorkbookNextIDSuffix(base) { var containerName = base + 's', regexp = /(workflow|action)([0-9]+)/, - container = workbook.get(containerName); + container = $scope.workbook.get(containerName); if ( !container ) { throw 'Base should be either "action" or "workflow"!'; } @@ -33,13 +33,15 @@ $scope.addAction = function() { var nextSuffix = getWorkbookNextIDSuffix(baseActionId), newID = baseActionId + nextSuffix; - workbook.get('actions').push({name: 'Action ' + nextSuffix}, {id: newID}); + $scope.workbook.get('actions').push( + {name: 'Action ' + nextSuffix}, {id: newID}); }; $scope.addWorkflow = function() { var nextSuffix = getWorkbookNextIDSuffix(baseWorkflowId), newID = baseWorkflowId + nextSuffix; - workbook.get('workflows').push({name: 'Workflow ' + nextSuffix}, {id: newID}); + $scope.workbook.get('workflows').push( + {name: 'Workflow ' + nextSuffix}, {id: newID}); }; }]) diff --git a/extensions/mistral/static/mistral/js/mistral.workbook.models.js b/extensions/mistral/static/mistral/js/mistral.workbook.models.js index 89622ce..e9522ab 100644 --- a/extensions/mistral/static/mistral/js/mistral.workbook.models.js +++ b/extensions/mistral/static/mistral/js/mistral.workbook.models.js @@ -197,6 +197,15 @@ }); models.Task = fields.frozendict.extend({ + create: function(json, parameters) { + var self = fields.frozendict.create.call(this, json, parameters); + self.on('childChange', function(child, op) { + if ( child === self.get('type') && op !== 'id' ) { + self.emit('change', 'taskType'); + } + }); + return self; + }, _getPrettyJSON: function() { var json = fields.frozendict._getPrettyJSON.apply(this, arguments); delete json.name; @@ -219,32 +228,27 @@ }, 'type': { '@class': fields.string.extend({}, { - '@enum': ['Action-based', 'Workflow-based'], + '@enum': [{ + value: 'action', label: 'Action-based' + }, { + value: 'workflow', label: 'Workflow-based' + }], + '@default': 'action', '@meta': { 'index': 1, 'row': 0 } }) }, - 'action': { - '@class': fields.string.extend({}, { + 'description': { + '@class': fields.text.extend({}, { '@meta': { - 'index': 2, + 'index': 1, 'row': 1 } }) }, 'input': { - '@class': fields.dictionary.extend({}, { - '@meta': { - 'index': 3 - }, - '?': { - '@class': fields.string - } - }) - }, - 'publish': { '@class': fields.dictionary.extend({}, { '@meta': { 'index': 4 @@ -254,35 +258,12 @@ } }) }, - 'on-error': { - '@class': fields.list.extend({}, { + 'publish': { + '@class': fields.dictionary.extend({}, { '@meta': { - 'title': 'On error', 'index': 5 }, - '*': { - '@class': fields.string - } - }) - }, - 'on-success': { - '@class': fields.list.extend({}, { - '@meta': { - 'title': 'On success', - 'index': 6 - }, - '*': { - '@class': fields.string - } - }) - }, - 'on-complete': { - '@class': fields.list.extend({}, { - '@meta': { - 'title': 'On complete', - 'index': 7 - }, - '*': { + '?': { '@class': fields.string } }) @@ -300,7 +281,7 @@ } }, { '@meta': { - 'index': 8 + 'index': 9 }, '@required': false, 'wait-before': { @@ -366,6 +347,94 @@ } }); + models.ReverseWFTask = models.Task.extend({}, { + 'requires': { + '@class': fields.string.extend({}, { + '@meta': { + 'row': 2, + 'index': 3 + } + }) + } + }); + + models.DirectWFTask = models.Task.extend({}, { + 'on-error': { + '@class': fields.list.extend({}, { + '@meta': { + 'title': 'On error', + 'index': 6 + }, + '*': { + '@class': fields.string + } + }) + }, + 'on-success': { + '@class': fields.list.extend({}, { + '@meta': { + 'title': 'On success', + 'index': 7 + }, + '*': { + '@class': fields.string + } + }) + }, + 'on-complete': { + '@class': fields.list.extend({}, { + '@meta': { + 'title': 'On complete', + 'index': 8 + }, + '*': { + '@class': fields.string + } + }) + } + }); + + models.ActionTaskMixin = Barricade.Blueprint.create(function() { + return this.extend({}, { + 'action': { + '@class': fields.string.extend({}, { + '@meta': { + 'row': 1, + 'index': 2 + } + }) + } + }); + }); + + models.WorkflowTaskMixin = Barricade.Blueprint.create(function() { + return this.extend({}, { + 'workflow': { + '@class': fields.string.extend({}, { + '@meta': { + 'row': 1, + 'index': 2 + } + }) + } + }); + }); + + var taskTypes = { + 'direct': models.DirectWFTask, + 'reverse': models.ReverseWFTask, + 'action': models.ActionTaskMixin, + 'workflow': models.WorkflowTaskMixin + }; + + function TaskFactory(json, parameters) { + var type = json.type || 'action', + baseClass = taskTypes[parameters.wfType], + mixinClass = taskTypes[type], + taskClass = mixinClass.call(baseClass); + return taskClass.create(json, parameters); + } + models.Workflow = fields.frozendict.extend({ create: function(json, parameters) { var self = fields.frozendict.create.call(this, json, parameters); @@ -423,13 +492,29 @@ }) }, 'tasks': { - '@class': fields.dictionary.extend({}, { + '@class': fields.dictionary.extend({ + create: function(json, parameters) { + var self = fields.dictionary.create.call(this, json, parameters); + self.on('childChange', function(child, op) { + if ( op === 'taskType' ) { + var taskId = child.getID(), + params = child._parameters, + taskPos = self.getPosByID(taskId), + taskData = child.toJSON(); + params.id = taskId; + self.set(taskPos, TaskFactory(taskData, params)); + } + }); + return self; + } + }, { '@meta': { 'index': 5, 'group': true }, '?': { - '@class': models.Task + '@class': models.Task, + '@factory': TaskFactory } }) } @@ -481,6 +566,7 @@ function workflowFactory(json, parameters) { var type = json.type || 'direct'; + parameters.wfType = type; return workflowTypes[type].create(json, parameters); } @@ -539,11 +625,11 @@ if ( op === 'workflowType' ) { var workflowId = child.getID(), workflowPos = self.getPosByID(workflowId), - workflowData = child.toJSON(), - newType = child.get('type').get(), - newWorkflow = workflowTypes[newType].create( - workflowData, {id: workflowId}); - self.set(workflowPos, newWorkflow); + params = child._parameters, + workflowData = child.toJSON(); + params.wfType = child.type; + params.id = workflowId; + self.set(workflowPos, workflowFactory(workflowData, params)); } }); return self; diff --git a/extensions/mistral/test/js/workbookSpec.js b/extensions/mistral/test/js/workbookSpec.js new file mode 100644 index 0000000..bc0fca0 --- /dev/null +++ b/extensions/mistral/test/js/workbookSpec.js @@ -0,0 +1,223 @@ + + /* Copyright (c) 2014 Mirantis, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); you may + not use this file except in compliance with the License. You may obtain + a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + License for the specific language governing permissions and limitations + under the License. +*/ +describe('workbook model logic', function() { + var models, utils, workbook; + + beforeEach(function() { + module('mistral'); + inject(function($injector) { + models = $injector.get('mistral.workbook.models'); + utils = $injector.get('merlin.utils'); + }); + workbook = models.Workbook.create(); + }); + + function getWorkflow(workflowID) { + return workbook.get('workflows').getByID(workflowID); + } + + describe('defines workflow structure transformations:', function() { + var workflowID = 'workflow1'; + + beforeEach(function() { + workbook.get('workflows').push({name: 'Workflow 1'}, {id: workflowID}); + }); + + it("new workflow starts as a 'direct' workflow and has proper structure", function() { + var workflow = getWorkflow(workflowID); + + expect(workflow.get('type').get()).toEqual('direct'); + expect(workflow.instanceof(models.DirectWorkflow)).toBe(true); + }); + + it("after setting type to 'reverse' the workflow structure changes to the proper one", function() { + getWorkflow(workflowID).get('type').set('reverse'); + + expect(getWorkflow(workflowID).instanceof(models.ReverseWorkflow)).toBe(true); + }); + + it("changing 'reverse' type to 'direct' again causes workflow structure to properly change", function() { + getWorkflow(workflowID).get('type').set('reverse'); + getWorkflow(workflowID).get('type').set('direct'); + + expect(getWorkflow(workflowID).instanceof(models.DirectWorkflow)).toBe(true); + }); + }); + + describe('defines task structure transformations', function() { + var workflowID = 'workflow1', + taskID = 'task1'; + + function getTask(taskID) { + return getWorkflow(workflowID).get('tasks').getByID(taskID); + } + + beforeEach(function() { + workbook.get('workflows').push({name: 'Workflow 1'}, {id: workflowID}); + }); + + describe("which start with the 'direct' workflow:", function() { + beforeEach(function() { + var workflow = getWorkflow(workflowID), + params = workflow._parameters; + workflow.get('tasks').push({name: 'Task 1'}, utils.extend(params, {id: taskID})); + }); + + it("new task starts as an 'action'-based one and has proper structure", function() { + expect(getTask(taskID).get('type').get()).toEqual('action'); + expect(getTask(taskID).instanceof(models.ActionTaskMixin)).toBe(true); + expect(getTask(taskID).instanceof(models.DirectWFTask)).toBe(true); + }); + + it("changing task type from 'action' to 'workflow' causes proper structure changes", function() { + getTask(taskID).get('type').set('workflow'); + + expect(getTask(taskID).instanceof(models.WorkflowTaskMixin)).toBe(true); + expect(getTask(taskID).instanceof(models.DirectWFTask)).toBe(true); + }); + + it("changing workflow type to 'reverse' causes the proper changes to its tasks", function() { + getWorkflow(workflowID).get('type').set('reverse'); + + expect(getTask(taskID).instanceof(models.ActionTaskMixin)).toBe(true); + expect(getTask(taskID).instanceof(models.ReverseWFTask)).toBe(true); + + getTask(taskID).get('type').set('workflow'); + + expect(getTask(taskID).instanceof(models.WorkflowTaskMixin)).toBe(true); + expect(getTask(taskID).instanceof(models.ReverseWFTask)).toBe(true); + }); + + it("changing workflow type from 'reverse' to 'direct' causes the proper changes to its tasks", function() { + getWorkflow(workflowID).get('type').set('reverse'); + getWorkflow(workflowID).get('type').set('direct'); + + expect(getTask(taskID).instanceof(models.ActionTaskMixin)).toBe(true); + expect(getTask(taskID).instanceof(models.DirectWFTask)).toBe(true); + + getTask(taskID).get('type').set('workflow'); + + expect(getTask(taskID).instanceof(models.WorkflowTaskMixin)).toBe(true); + expect(getTask(taskID).instanceof(models.DirectWFTask)).toBe(true); + }); + }); + + describe("which start with the 'reverse' workflow:", function() { + beforeEach(function() { + var workflow; + getWorkflow(workflowID).get('type').set('reverse'); + workflow = getWorkflow(workflowID); + workflow.get('tasks').push( + {name: 'Task 1'}, utils.extend(workflow._parameters, {id: taskID})); + }); + + it("new task starts as an 'action'-based one and has proper structure", function() { + expect(getTask(taskID).get('type').get()).toEqual('action'); + expect(getTask(taskID).instanceof(models.ActionTaskMixin)).toBe(true); + expect(getTask(taskID).instanceof(models.ReverseWFTask)).toBe(true); + }); + + it("changing task type from 'action' to 'workflow' causes proper structure changes", function() { + getTask(taskID).get('type').set('workflow'); + + expect(getTask(taskID).instanceof(models.WorkflowTaskMixin)).toBe(true); + expect(getTask(taskID).instanceof(models.ReverseWFTask)).toBe(true); + }); + + it("changing workflow type to 'direct' causes the proper changes to its tasks", function() { + getWorkflow(workflowID).get('type').set('direct'); + + expect(getTask(taskID).instanceof(models.ActionTaskMixin)).toBe(true); + expect(getTask(taskID).instanceof(models.DirectWFTask)).toBe(true); + + getTask(taskID).get('type').set('workflow'); + + expect(getTask(taskID).instanceof(models.WorkflowTaskMixin)).toBe(true); + expect(getTask(taskID).instanceof(models.DirectWFTask)).toBe(true); + }); + + it("changing workflow type from 'direct' to 'reverse' causes the proper changes to its tasks", function() { + getWorkflow(workflowID).get('type').set('direct'); + getWorkflow(workflowID).get('type').set('reverse'); + + expect(getTask(taskID).instanceof(models.ActionTaskMixin)).toBe(true); + expect(getTask(taskID).instanceof(models.ReverseWFTask)).toBe(true); + + getTask(taskID).get('type').set('workflow'); + + expect(getTask(taskID).instanceof(models.WorkflowTaskMixin)).toBe(true); + expect(getTask(taskID).instanceof(models.ReverseWFTask)).toBe(true); + }); + }); + }); + + describe('defines top-level actions available to user:', function() { + var $scope; + + beforeEach(inject(function(_$controller_) { + var $controller = _$controller_; + $scope = {}; + $controller('workbookCtrl', {$scope: $scope}); + $scope.workbook = workbook; + })); + + describe("'Add Action' action", function() { + it('adds a new Action', function() { + $scope.addAction(); + + expect(workbook.get('actions').get(0)).toBeDefined(); + }); + + it('creates action with predefined name', function() { + $scope.addAction(); + + expect(workbook.get('actions').get(0).get('name').get()).toBeGreaterThan(''); + }); + + it('creates actions with different names on 2 successive calls', function() { + $scope.addAction(); + $scope.addAction(); + + expect(workbook.get('actions').get(0).get('name').get()).not.toEqual( + workbook.get('actions').get(1).get('name').get()) + }); + }); + + describe("'Add Workflow' action", function() { + it('adds a new Workflow', function() { + $scope.addWorkflow(); + + expect(workbook.get('workflows').get(0)).toBeDefined(); + }); + + it('creates workflow with predefined name', function() { + $scope.addWorkflow(); + + expect(workbook.get('workflows').get(0).get('name').get()).toBeGreaterThan(''); + }); + + it('creates workflows with different names on 2 successive calls', function() { + $scope.addWorkflow(); + $scope.addWorkflow(); + + expect(workbook.get('workflows').get(0).get('name').get()).not.toEqual( + workbook.get('workflows').get(1).get('name').get()) + }); + + }); + + }) +}); \ No newline at end of file diff --git a/karma-unit.conf.js b/karma-unit.conf.js index 76b90d0..d30d296 100644 --- a/karma-unit.conf.js +++ b/karma-unit.conf.js @@ -33,22 +33,27 @@ module.exports = function (config) { ], files: [ - './bower_components/angular/angular.js', - './bower_components/angular-mocks/angular-mocks.js', - './merlin/static/merlin/js/lib/underscore-min.js', - './merlin/static/merlin/js/merlin.init.js', - './merlin/static/merlin/js/merlin.templates.js', - './merlin/static/merlin/js/merlin.directives.js', - './merlin/static/merlin/js/merlin.filters.js', - './merlin/static/merlin/js/merlin.field.models.js', - './merlin/static/merlin/js/merlin.panel.models.js', - './merlin/static/merlin/js/merlin.utils.js', - './merlin/static/merlin/js/lib/angular-filter.js', - './merlin/static/merlin/js/lib/barricade.js', - './merlin/static/merlin/js/lib/js-yaml.js', + 'bower_components/angular/angular.js', + 'bower_components/angular-mocks/angular-mocks.js', + 'merlin/static/merlin/js/lib/underscore-min.js', + 'merlin/static/merlin/js/merlin.init.js', + 'merlin/static/merlin/js/merlin.templates.js', + 'merlin/static/merlin/js/merlin.directives.js', + 'merlin/static/merlin/js/merlin.filters.js', + 'merlin/static/merlin/js/merlin.field.models.js', + 'merlin/static/merlin/js/merlin.panel.models.js', + 'merlin/static/merlin/js/merlin.utils.js', + 'merlin/static/merlin/js/lib/angular-filter.js', + 'merlin/static/merlin/js/lib/barricade.js', + 'merlin/static/merlin/js/lib/js-yaml.js', 'merlin/test/js/utilsSpec.js', 'merlin/test/js/templatesSpec.js', - 'merlin/test/js/filtersSpec.js' + 'merlin/test/js/filtersSpec.js', + + 'extensions/mistral/static/mistral/js/mistral.init.js', + 'extensions/mistral/static/mistral/js/mistral.workbook.models.js', + 'extensions/mistral/static/mistral/js/mistral.workbook.controllers.js', + 'extensions/mistral/test/js/workbookSpec.js' ], exclude: [ diff --git a/merlin/static/merlin/js/lib/barricade.js b/merlin/static/merlin/js/lib/barricade.js index 127341d..23e68f7 100644 --- a/merlin/static/merlin/js/lib/barricade.js +++ b/merlin/static/merlin/js/lib/barricade.js @@ -47,11 +47,12 @@ var Barricade = (function () { */ create: function (f) { return function g() { - if (!Object.prototype.hasOwnProperty.call(this, '_parents')) { - Object.defineProperty(this, '_parents', {value: []}); + var result = f.apply(this, arguments) || this; + if (!Object.prototype.hasOwnProperty.call(result, '_parents')) { + Object.defineProperty(result, '_parents', {value: []}); } - this._parents.push(g); - return f.apply(this, arguments); + result._parents.push(g); + return result; }; } }; @@ -547,7 +548,7 @@ var Barricade = (function () { create: function (json, parameters) { var self = this.extend({}), schema = self._schema, - isUsed; + isUsed, id; self._parameters = parameters = parameters || {}; @@ -570,7 +571,10 @@ var Barricade = (function () { Enumerated.call(self, schema['@enum']); } - Identifiable.call(self, parameters.id); + if ( Object.hasOwnProperty.call(parameters, 'id') ) { + id = parameters.id; + } + Identifiable.call(self, id); return self; }, @@ -871,10 +875,10 @@ var Barricade = (function () { * @memberof Barricade.Arraylike * @private */ - _sift: function (json) { + _sift: function (json, parameters) { return json.map(function (el) { return this._keyClassCreate( - this._elSymbol, this._elementClass, el); + this._elSymbol, this._elementClass, el, parameters); }, this); }, @@ -1030,11 +1034,11 @@ var Barricade = (function () { * @memberof Barricade.ImmutableObject * @private */ - _sift: function (json) { + _sift: function (json, parameters) { var self = this; return this.getKeys().reduce(function (objOut, key) { - objOut[key] = - self._keyClassCreate(key, self._keyClasses[key], json[key]); + objOut[key] = self._keyClassCreate( + key, self._keyClasses[key], json[key], parameters); return objOut; }, {}); }, @@ -1171,10 +1175,12 @@ var Barricade = (function () { * @memberof Barricade.MutableObject * @private */ - _sift: function (json) { + _sift: function (json, parameters) { return Object.keys(json).map(function (key) { - return this._keyClassCreate(this._elSymbol, this._elementClass, - json[key], {id: key}); + var params = Object.create(parameters); + params.id = key; + return this._keyClassCreate( + this._elSymbol, this._elementClass, json[key], params); }, this); }, diff --git a/merlin/static/merlin/js/merlin.field.models.js b/merlin/static/merlin/js/merlin.field.models.js index 2b108b9..35c9b67 100644 --- a/merlin/static/merlin/js/merlin.field.models.js +++ b/merlin/static/merlin/js/merlin.field.models.js @@ -123,7 +123,7 @@ modelMixin.call(self, 'list'); self.add = function() { - self.push(); + self.push(undefined, parameters); }; self.getValues = function() { return self.toArray(); @@ -181,7 +181,7 @@ } else { // usually, it's either frozendict inside or string newValue = ''; } - self.push(newValue, {id: newID}); + self.push(newValue, utils.extend(self._parameters, {id: newID})); _items[newID] = self.getByID(newID); }; self.getValues = function() { diff --git a/merlin/static/merlin/js/merlin.utils.js b/merlin/static/merlin/js/merlin.utils.js index 0b0685a..8ea7b5d 100644 --- a/merlin/static/merlin/js/merlin.utils.js +++ b/merlin/static/merlin/js/merlin.utils.js @@ -82,6 +82,16 @@ } } + function extend(proto, extension) { + var newObj; + proto = (proto !== undefined ? proto : null); + newObj = Object.create(proto); + Object.keys(extension).forEach(function(key) { + newObj[key] = extension[key]; + }); + return newObj; + } + return { getMeta: getMeta, getNewId: getNewId, @@ -89,6 +99,7 @@ makeTitle: makeTitle, getNextIDSuffix: getNextIDSuffix, enhanceItemWithID: enhanceItemWithID, + extend: extend, pop: pop } }) diff --git a/merlin/test/js/utilsSpec.js b/merlin/test/js/utilsSpec.js index 28280da..c995c3a 100644 --- a/merlin/test/js/utilsSpec.js +++ b/merlin/test/js/utilsSpec.js @@ -27,7 +27,41 @@ describe('merlin.utils', function() { expect(array.condense()).toEqual([1, 0, 15, 7, 8]); }); }); + function extend(proto, extension) { + var newObj; + proto = (proto !== undefined ? proto : null); + newObj = Object.create(proto); + Object.keys(extension).forEach(function(key) { + newObj[key] = extension[key]; + }); + return newObj; + } + describe('extend function', function() { + var obj; + beforeEach(function() { + obj = { + 'key1': 10, + 'key2': 20 + }; + }); + it("doesn't remove existing keys from the resulting object", function() { + var newObj = extend(obj, {'key3': 30}); + expect(newObj.key1).toBe(10); + expect(newObj.key3).toBe(30); + }); + + it('overrides keys with the same names as the ones in extension', function() { + var newObj = extend(obj, {'key2': 40}); + expect(newObj.key2).toBe(40); + }); + + it("doesn't touch the original object, even the keys with the same names", function() { + var newObj = extend(obj, {'key2': 40, 'key4': 50}); + expect(obj.key1).toBe(10); + expect(obj.key2).toBe(20); + }); + }) });