summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTimur Sufiev <tsufiev@mirantis.com>2015-06-25 19:05:35 +0300
committerTimur Sufiev <tsufiev@mirantis.com>2015-06-26 20:23:13 +0300
commita5c1c308cfa44bd99b906078ec650478a0da05dc (patch)
treeac7a83e156a6eb931263aca6afa15b02e09aa218
parent7ccf4f0dd35a055cec67e588688f58edf4ba8148 (diff)
Decouple @enum and drop-down widget
Provide a convenience fields.linkedcollection model to handle common use-case of using @ref in a Mistral WB. Cover it with unit-tests as well all scenarios of using fields.linkedcollection in MIstral WB. Change-Id: I97a61262db4cc521b5c230667a49b99701318f3f Closes-Bug: #1467514
Notes
Notes (review): Verified+2: Jenkins Code-Review+2: Timur Sufiev <tsufiev@mirantis.com> Workflow+1: Timur Sufiev <tsufiev@mirantis.com> Submitted-by: Jenkins Submitted-at: Fri, 26 Jun 2015 18:01:13 +0000 Reviewed-on: https://review.openstack.org/195655 Project: stackforge/merlin Branch: refs/heads/master
-rw-r--r--extensions/mistral/static/mistral/js/mistral.workbook.models.js131
-rw-r--r--extensions/mistral/test/js/workbook.controller.spec.js143
-rw-r--r--extensions/mistral/test/js/workbookSpec.js201
-rw-r--r--karma-unit.conf.js3
-rw-r--r--merlin/static/merlin/js/merlin.directives.js16
-rw-r--r--merlin/static/merlin/js/merlin.field.models.js76
-rw-r--r--merlin/static/merlin/templates/fields/choices.html11
-rw-r--r--merlin/static/merlin/templates/fields/number.html2
-rw-r--r--merlin/static/merlin/templates/fields/string.html2
-rw-r--r--merlin/static/merlin/templates/fields/text.html2
-rw-r--r--merlin/test/js/modelsSpec.js61
11 files changed, 374 insertions, 274 deletions
diff --git a/extensions/mistral/static/mistral/js/mistral.workbook.models.js b/extensions/mistral/static/mistral/js/mistral.workbook.models.js
index d4b3a95..9a95df7 100644
--- a/extensions/mistral/static/mistral/js/mistral.workbook.models.js
+++ b/extensions/mistral/static/mistral/js/mistral.workbook.models.js
@@ -110,10 +110,10 @@
110 base.on('change', function(operation) { 110 base.on('change', function(operation) {
111 var argsEntry, pos, entry; 111 var argsEntry, pos, entry;
112 if ( operation != 'id' ) { 112 if ( operation != 'id' ) {
113 pos = base._stdActions.getPosByID(base.get()); 113 pos = base._collection.getPosByID(base.get());
114 if ( pos > -1 ) { 114 if ( pos > -1 ) {
115 entry = self.get('base-input'); 115 entry = self.get('base-input');
116 argsEntry = base._stdActions.get(pos); 116 argsEntry = base._collection.get(pos);
117 entry.resetKeys(argsEntry.toJSON()); 117 entry.resetKeys(argsEntry.toJSON());
118 } 118 }
119 } 119 }
@@ -122,44 +122,15 @@
122 } 122 }
123 }, { 123 }, {
124 'base': { 124 'base': {
125 '@class': fields.string.extend({ 125 '@class': fields.linkedcollection.extend({
126 create: function(json, parameters) { 126 create: function(json, parameters) {
127 var self = fields.string.create.call(this, json, parameters), 127 parameters = Object.create(parameters);
128 stdActionsCls = Barricade.create({ 128 parameters.toCls = models.StandardActions;
129 '@type': String, 129 parameters.neededCls = models.Root;
130 '@ref': { 130 parameters.substitutedEntryID = 'standardActions';
131 to: function() { 131 return fields.linkedcollection.create.call(this, json, parameters);
132 return fields.StandardActions; 132 }
133 },
134 needs: function() {
135 return models.Root;
136 },
137 getter: function(data) {
138 return data.needed.get('standardActions');
139 }
140 }
141 });
142
143 self._stdActions = stdActionsCls.create().on(
144 'replace', function(newValue) {
145 self._stdActions = newValue;
146 self._stdActions.on('change', function() {
147 self._choices = self._stdActions.getIDs();
148 self.resetValues();
149 });
150 self._stdActions.emit('change');
151 });
152
153 return self;
154 },
155 _choices: []
156 }, { 133 }, {
157 '@enum': function() {
158 if ( this._stdActions.isPlaceholder() ) {
159 this.emit('_resolveUp', this._stdActions);
160 }
161 return this._choices;
162 },
163 '@meta': { 134 '@meta': {
164 'index': 1, 135 'index': 1,
165 'row': 0 136 'row': 0
@@ -402,43 +373,15 @@
402 models.ActionTaskMixin = Barricade.Blueprint.create(function() { 373 models.ActionTaskMixin = Barricade.Blueprint.create(function() {
403 return this.extend({}, { 374 return this.extend({}, {
404 'action': { 375 'action': {
405 '@class': fields.string.extend({ 376 '@class': fields.linkedcollection.extend({
406 create: function(json, parameters) { 377 create: function(json, parameters) {
407 var self = fields.string.create.call(this, json, parameters), 378 parameters = Object.create(parameters);
408 actionsCls = Barricade.create({ 379 parameters.toCls = models.Actions;
409 '@type': String, 380 parameters.neededCls = models.Workbook;
410 '@ref': { 381 parameters.substitutedEntryID = 'actions';
411 to: function() { 382 return fields.linkedcollection.create.call(this, json, parameters);
412 return models.Actions; 383 }
413 },
414 needs: function() {
415 return models.Workbook;
416 },
417 getter: function(data) {
418 return data.needed.get('actions');
419 }
420 }
421 });
422
423 self._actions = actionsCls.create().on(
424 'replace', function(newValue) {
425 self._actions = newValue;
426 self._actions.on('change', function() {
427 self._choices = self._actions.getIDs();
428 self.resetValues();
429 });
430 self._actions.emit('change');
431 });
432 return self;
433 },
434 _choices: []
435 }, { 384 }, {
436 '@enum': function() {
437 if ( this._actions.isPlaceholder() ) {
438 this.emit('_resolveUp', this._actions);
439 }
440 return this._choices;
441 },
442 '@meta': { 385 '@meta': {
443 'row': 0, 386 'row': 0,
444 'index': 1 387 'index': 1
@@ -451,43 +394,15 @@
451 models.WorkflowTaskMixin = Barricade.Blueprint.create(function() { 394 models.WorkflowTaskMixin = Barricade.Blueprint.create(function() {
452 return this.extend({}, { 395 return this.extend({}, {
453 'workflow': { 396 'workflow': {
454 '@class': fields.string.extend({ 397 '@class': fields.linkedcollection.extend({
455 create: function(json, parameters) { 398 create: function(json, parameters) {
456 var self = fields.string.create.call(this, json, parameters), 399 parameters = Object.create(parameters);
457 workflowsCls = Barricade.create({ 400 parameters.toCls = models.Workflows;
458 '@type': String, 401 parameters.neededCls = models.Workbook;
459 '@ref': { 402 parameters.substitutedEntryID = 'workflows';
460 to: function() { 403 return fields.linkedcollection.create.call(this, json, parameters);
461 return models.Workflows; 404 }
462 },
463 needs: function() {
464 return models.Workbook;
465 },
466 getter: function(data) {
467 return data.needed.get('workflows');
468 }
469 }
470 });
471
472 self._workflows = workflowsCls.create().on(
473 'replace', function(newValue) {
474 self._workflows = newValue;
475 self._workflows.on('change', function() {
476 self._choices = self._workflows.getIDs();
477 self.resetValues();
478 });
479 self._workflows.emit('change');
480 });
481 return self;
482 },
483 _choices: []
484 }, { 405 }, {
485 '@enum': function() {
486 if ( this._workflows.isPlaceholder() ) {
487 this.emit('_resolveUp', this._workflows);
488 }
489 return this._choices;
490 },
491 '@meta': { 406 '@meta': {
492 'row': 0, 407 'row': 0,
493 'index': 1 408 'index': 1
diff --git a/extensions/mistral/test/js/workbook.controller.spec.js b/extensions/mistral/test/js/workbook.controller.spec.js
new file mode 100644
index 0000000..92317fb
--- /dev/null
+++ b/extensions/mistral/test/js/workbook.controller.spec.js
@@ -0,0 +1,143 @@
1
2/* Copyright (c) 2015 Mirantis, Inc.
3
4 Licensed under the Apache License, Version 2.0 (the "License"); you may
5 not use this file except in compliance with the License. You may obtain
6 a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 License for the specific language governing permissions and limitations
14 under the License.
15 */
16describe('together workbook model and controller', function() {
17 var models, utils, workbook;
18
19 beforeEach(function () {
20 module('mistral');
21 inject(function ($injector) {
22 models = $injector.get('mistral.workbook.models');
23 utils = $injector.get('merlin.utils');
24 });
25 workbook = models.Workbook.create();
26 });
27
28
29 describe('define top-level actions available to user:', function () {
30 var $scope;
31
32 beforeEach(inject(function (_$controller_) {
33 var $controller = _$controller_;
34 $scope = {};
35 $controller('workbookCtrl', {$scope: $scope});
36 $scope.workbook = workbook;
37 }));
38
39 describe("'Add Action' action", function () {
40 it('adds a new Action', function () {
41 $scope.addAction();
42
43 expect(workbook.get('actions').get(0)).toBeDefined();
44 });
45
46 it('creates action with predefined name', function () {
47 $scope.addAction();
48
49 expect(workbook.get('actions').get(0).getID()).toBeGreaterThan('');
50 });
51
52 describe('', function () {
53 var actionID;
54 beforeEach(inject(function (baseActionID) {
55 actionID = baseActionID + '1';
56 }));
57
58 it("corresponding JSON has the right key for the Action", function () {
59 $scope.addAction();
60
61 expect(workbook.toJSON({pretty: true}).actions[actionID]).toBeDefined();
62 });
63
64 it("once the Action ID is changed, it's reflected in JSON", function () {
65 var newID = 'action10';
66
67 $scope.addAction();
68 workbook.get('actions').getByID(actionID).setID(newID);
69
70 expect(workbook.toJSON({pretty: true}).actions[actionID]).toBeUndefined();
71 expect(workbook.toJSON({pretty: true}).actions[newID]).toBeDefined();
72 });
73
74 });
75
76 it('creates actions with different names on 2 successive calls', function () {
77 $scope.addAction();
78 $scope.addAction();
79
80 expect(workbook.get('actions').get(0).getID()).not.toEqual(
81 workbook.get('actions').get(1).getID())
82 });
83 });
84
85 describe("'Add Workflow' action", function () {
86 it('adds a new Workflow', function () {
87 $scope.addWorkflow();
88
89 expect(workbook.get('workflows').get(0)).toBeDefined();
90 });
91
92 describe('', function () {
93 var workflowID;
94 beforeEach(inject(function (baseWorkflowID) {
95 workflowID = baseWorkflowID + '1';
96 }));
97
98 it("corresponding JSON has the right key for the Workflow", function () {
99 $scope.addWorkflow();
100
101 expect(workbook.toJSON({pretty: true}).workflows[workflowID]).toBeDefined();
102 });
103
104 it("once the workflow ID is changed, it's reflected in JSON", function () {
105 var newID = 'workflow10';
106
107 $scope.addWorkflow();
108 workbook.get('workflows').getByID(workflowID).setID(newID);
109
110 expect(workbook.toJSON({pretty: true}).workflows[workflowID]).toBeUndefined();
111 expect(workbook.toJSON({pretty: true}).workflows[newID]).toBeDefined();
112 });
113
114 });
115
116 it('creates workflow with predefined name', function () {
117 $scope.addWorkflow();
118
119 expect(workbook.get('workflows').get(0).getID()).toBeGreaterThan('');
120 });
121
122 it('creates workflows with different names on 2 successive calls', function () {
123 $scope.addWorkflow();
124 $scope.addWorkflow();
125
126 expect(workbook.get('workflows').get(0).getID()).not.toEqual(
127 workbook.get('workflows').get(1).getID())
128 });
129
130 });
131
132 describe("'Create'/'Modify'/'Cancel' actions", function () {
133 it('edit causes a request to an api and a return to main page', function () {
134
135 });
136
137 it('cancel causes just a return to main page', function () {
138
139 });
140 });
141
142 })
143}); \ No newline at end of file
diff --git a/extensions/mistral/test/js/workbookSpec.js b/extensions/mistral/test/js/workbookSpec.js
index 504ef25..5e80297 100644
--- a/extensions/mistral/test/js/workbookSpec.js
+++ b/extensions/mistral/test/js/workbookSpec.js
@@ -31,6 +31,37 @@ describe('workbook model logic', function() {
31 return workbook.get('workflows').getByID(workflowID); 31 return workbook.get('workflows').getByID(workflowID);
32 } 32 }
33 33
34 describe('defines the standard actions getter for Action->Base field:', function() {
35 var root, action1;
36
37 beforeEach(function() {
38 root = models.Root.create();
39 root.set('workbook', workbook);
40 root.set('standardActions', {
41 'nova.create_server': ['image', 'flavor', 'network_id'],
42 'neutron.create_network': ['name', 'create_subnet'],
43 'glance.create_image': ['image_url']
44 });
45 workbook.get('actions').add('action1');
46 action1 = workbook.get('actions').getByID('action1');
47 });
48
49 it('all actions are present as choices for the Base field', function() {
50 var availableActions = action1.get('base').getValues();
51
52 expect(availableActions).toEqual([
53 'nova.create_server', 'neutron.create_network', 'glance.create_image']);
54 });
55
56 it("'Base Input' field is set to have keys corresponding to 'Base' field value", function() {
57 action1.get('base').set('nova.create_server');
58 expect(action1.get('base-input').getIDs()).toEqual(['image', 'flavor', 'network_id']);
59
60 action1.get('base').set('neutron.create_network');
61 expect(action1.get('base-input').getIDs()).toEqual(['name', 'create_subnet']);
62 });
63 });
64
34 describe('defines workflow structure transformations:', function() { 65 describe('defines workflow structure transformations:', function() {
35 var workflowID = 'workflow1'; 66 var workflowID = 'workflow1';
36 67
@@ -71,7 +102,7 @@ describe('workbook model logic', function() {
71 } 102 }
72 103
73 beforeEach(function() { 104 beforeEach(function() {
74 workbook.get('workflows').push({name: 'Workflow 1'}, {id: workflowID}); 105 workbook.get('workflows').add(workflowID);
75 }); 106 });
76 107
77 describe('', function() { 108 describe('', function() {
@@ -120,11 +151,30 @@ describe('workbook model logic', function() {
120 expect(getTask(taskID).instanceof(models.DirectWFTask)).toBe(true); 151 expect(getTask(taskID).instanceof(models.DirectWFTask)).toBe(true);
121 }); 152 });
122 153
123 it("changing task type from 'action' to 'workflow' causes proper structure changes", function() { 154 it("'action'-based task offers available custom actions for its Action field", function() {
124 getTask(taskID).get('type').set('workflow'); 155 workbook.get('actions').add('action1');
156 expect(getTask(taskID).get('action').getValues()).toEqual(['action1']);
125 157
126 expect(getTask(taskID).instanceof(models.WorkflowTaskMixin)).toBe(true); 158 workbook.get('actions').add('action2');
127 expect(getTask(taskID).instanceof(models.DirectWFTask)).toBe(true); 159 expect(getTask(taskID).get('action').getValues()).toEqual(['action1', 'action2']);
160 });
161
162 describe("changing task type from 'action' to 'workflow' causes", function() {
163 beforeEach(function() {
164 getTask(taskID).get('type').set('workflow');
165 });
166
167 it('proper structure changes', function() {
168 expect(getTask(taskID).instanceof(models.WorkflowTaskMixin)).toBe(true);
169 expect(getTask(taskID).instanceof(models.DirectWFTask)).toBe(true);
170 });
171
172 it('and causes the Workflow field to suggest available workflows as choices', function() {
173 expect(getTask(taskID).get('workflow').getValues()).toEqual(['workflow1']);
174
175 workbook.get('workflows').add('workflow2');
176 expect(getTask(taskID).get('workflow').getValues()).toEqual([workflowID, 'workflow2']);
177 });
128 }); 178 });
129 179
130 it("changing workflow type to 'reverse' causes the proper changes to its tasks", function() { 180 it("changing workflow type to 'reverse' causes the proper changes to its tasks", function() {
@@ -168,11 +218,30 @@ describe('workbook model logic', function() {
168 expect(getTask(taskID).instanceof(models.ReverseWFTask)).toBe(true); 218 expect(getTask(taskID).instanceof(models.ReverseWFTask)).toBe(true);
169 }); 219 });
170 220
171 it("changing task type from 'action' to 'workflow' causes proper structure changes", function() { 221 it("'action'-based task offers available custom actions for its Action field", function() {
172 getTask(taskID).get('type').set('workflow'); 222 workbook.get('actions').add('action1');
223 expect(getTask(taskID).get('action').getValues()).toEqual(['action1']);
173 224
174 expect(getTask(taskID).instanceof(models.WorkflowTaskMixin)).toBe(true); 225 workbook.get('actions').add('action2');
175 expect(getTask(taskID).instanceof(models.ReverseWFTask)).toBe(true); 226 expect(getTask(taskID).get('action').getValues()).toEqual(['action1', 'action2']);
227 });
228
229 describe("changing task type from 'action' to 'workflow' causes", function() {
230 beforeEach(function() {
231 getTask(taskID).get('type').set('workflow');
232 });
233
234 it('proper structure changes', function() {
235 expect(getTask(taskID).instanceof(models.WorkflowTaskMixin)).toBe(true);
236 expect(getTask(taskID).instanceof(models.ReverseWFTask)).toBe(true);
237 });
238
239 it('and causes the Workflow field to suggest available workflows as choices', function() {
240 expect(getTask(taskID).get('workflow').getValues()).toEqual(['workflow1']);
241
242 workbook.get('workflows').add('workflow2');
243 expect(getTask(taskID).get('workflow').getValues()).toEqual([workflowID, 'workflow2']);
244 });
176 }); 245 });
177 246
178 it("changing workflow type to 'direct' causes the proper changes to its tasks", function() { 247 it("changing workflow type to 'direct' causes the proper changes to its tasks", function() {
@@ -202,118 +271,4 @@ describe('workbook model logic', function() {
202 }); 271 });
203 }); 272 });
204 273
205 describe('defines top-level actions available to user:', function() {
206 var $scope;
207
208 beforeEach(inject(function(_$controller_) {
209 var $controller = _$controller_;
210 $scope = {};
211 $controller('workbookCtrl', {$scope: $scope});
212 $scope.workbook = workbook;
213 }));
214
215 describe("'Add Action' action", function() {
216 it('adds a new Action', function() {
217 $scope.addAction();
218
219 expect(workbook.get('actions').get(0)).toBeDefined();
220 });
221
222 it('creates action with predefined name', function() {
223 $scope.addAction();
224
225 expect(workbook.get('actions').get(0).getID()).toBeGreaterThan('');
226 });
227
228 describe('', function() {
229 var actionID;
230 beforeEach(inject(function(baseActionID) {
231 actionID = baseActionID + '1';
232 }));
233
234 it("corresponding JSON has the right key for the Action", function() {
235 $scope.addAction();
236
237 expect(workbook.toJSON({pretty: true}).actions[actionID]).toBeDefined();
238 });
239
240 it("once the Action ID is changed, it's reflected in JSON", function() {
241 var newID = 'action10';
242
243 $scope.addAction();
244 workbook.get('actions').getByID(actionID).setID(newID);
245
246 expect(workbook.toJSON({pretty: true}).actions[actionID]).toBeUndefined();
247 expect(workbook.toJSON({pretty: true}).actions[newID]).toBeDefined();
248 });
249
250 });
251
252 it('creates actions with different names on 2 successive calls', function() {
253 $scope.addAction();
254 $scope.addAction();
255
256 expect(workbook.get('actions').get(0).getID()).not.toEqual(
257 workbook.get('actions').get(1).getID())
258 });
259 });
260
261 describe("'Add Workflow' action", function() {
262 it('adds a new Workflow', function() {
263 $scope.addWorkflow();
264
265 expect(workbook.get('workflows').get(0)).toBeDefined();
266 });
267
268 describe('', function() {
269 var workflowID;
270 beforeEach(inject(function(baseWorkflowID) {
271 workflowID = baseWorkflowID + '1';
272 }));
273
274 it("corresponding JSON has the right key for the Workflow", function() {
275 $scope.addWorkflow();
276
277 expect(workbook.toJSON({pretty: true}).workflows[workflowID]).toBeDefined();
278 });
279
280 it("once the workflow ID is changed, it's reflected in JSON", function() {
281 var newID = 'workflow10';
282
283 $scope.addWorkflow();
284 workbook.get('workflows').getByID(workflowID).setID(newID);
285
286 expect(workbook.toJSON({pretty: true}).workflows[workflowID]).toBeUndefined();
287 expect(workbook.toJSON({pretty: true}).workflows[newID]).toBeDefined();
288 });
289
290 });
291
292 it('creates workflow with predefined name', function() {
293 $scope.addWorkflow();
294
295 expect(workbook.get('workflows').get(0).getID()).toBeGreaterThan('');
296 });
297
298 it('creates workflows with different names on 2 successive calls', function() {
299 $scope.addWorkflow();
300 $scope.addWorkflow();
301
302 expect(workbook.get('workflows').get(0).getID()).not.toEqual(
303 workbook.get('workflows').get(1).getID())
304 });
305
306 });
307
308 describe("'Create'/'Modify'/'Cancel' actions", function() {
309 it('edit causes a request to an api and a return to main page', function() {
310
311 });
312
313 it('cancel causes just a return to main page', function() {
314
315 });
316 });
317
318 })
319}); 274});
diff --git a/karma-unit.conf.js b/karma-unit.conf.js
index 87b08ad..1990664 100644
--- a/karma-unit.conf.js
+++ b/karma-unit.conf.js
@@ -52,7 +52,8 @@ module.exports = function (config) {
52 // explicitly require first module definition file to avoid errors 52 // explicitly require first module definition file to avoid errors
53 'extensions/mistral/static/mistral/js/mistral.init.js', 53 'extensions/mistral/static/mistral/js/mistral.init.js',
54 'extensions/mistral/static/mistral/js/mistral.*.js', 54 'extensions/mistral/static/mistral/js/mistral.*.js',
55 'extensions/mistral/test/js/*Spec.js' 55 'extensions/mistral/test/js/*Spec.js',
56 'extensions/mistral/test/js/*spec.js'
56 ], 57 ],
57 58
58 preprocessors: { 59 preprocessors: {
diff --git a/merlin/static/merlin/js/merlin.directives.js b/merlin/static/merlin/js/merlin.directives.js
index 5f0f306..ef1ad6d 100644
--- a/merlin/static/merlin/js/merlin.directives.js
+++ b/merlin/static/merlin/js/merlin.directives.js
@@ -134,18 +134,6 @@
134 }) 134 })
135 .directive('typedField', ['$compile', 'merlin.templates', 135 .directive('typedField', ['$compile', 'merlin.templates',
136 function($compile, templates) { 136 function($compile, templates) {
137 function updateAutoCompletionDirective(template) {
138 template.find('input').each(function(index, elem) {
139 elem = angular.element(elem);
140 if ( elem.attr('autocompletable') ) {
141 // process 'autocompletable' attribute only once
142 elem.removeAttr('autocompletable');
143 elem.attr('typeahead-editable', true);
144 elem.attr('typeahead',
145 "option for option in value.getSuggestions() | filter:$viewValue");
146 }
147 });
148 }
149 return { 137 return {
150 restrict: 'E', 138 restrict: 'E',
151 scope: { 139 scope: {
@@ -154,10 +142,6 @@
154 }, 142 },
155 link: function(scope, element) { 143 link: function(scope, element) {
156 templates.templateReady(scope.type).then(function(template) { 144 templates.templateReady(scope.type).then(function(template) {
157 template = angular.element(template);
158 if ( scope.value.getSuggestions ) {
159 updateAutoCompletionDirective(template);
160 }
161 element.replaceWith($compile(template)(scope)); 145 element.replaceWith($compile(template)(scope));
162 }) 146 })
163 } 147 }
diff --git a/merlin/static/merlin/js/merlin.field.models.js b/merlin/static/merlin/js/merlin.field.models.js
index 6be62f3..4f84d7a 100644
--- a/merlin/static/merlin/js/merlin.field.models.js
+++ b/merlin/static/merlin/js/merlin.field.models.js
@@ -10,9 +10,10 @@
10 return this; 10 return this;
11 }); 11 });
12 12
13 var restrictedChoicesMixin = Barricade.Blueprint.create(function() { 13 var viewChoicesMixin = Barricade.Blueprint.create(function() {
14 var self = this, 14 var self = this,
15 values, labels, items; 15 dropDownLimit = this._dropDownLimit || 5,
16 values, labels, items, isDropDown;
16 17
17 function fillItems() { 18 function fillItems() {
18 values = self.getEnumValues(); 19 values = self.getEnumValues();
@@ -42,6 +43,15 @@
42 values = undefined; 43 values = undefined;
43 }; 44 };
44 45
46 this.isDropDown = function() {
47 // what starts its life as being dropdown / not being dropdown
48 // should remain so forever
49 if ( angular.isUndefined(isDropDown) ) {
50 isDropDown = !this.isEmpty() && this.getValues().length < dropDownLimit;
51 }
52 return isDropDown;
53 };
54
45 this.setType('choices'); 55 this.setType('choices');
46 return this; 56 return this;
47 }); 57 });
@@ -92,11 +102,7 @@
92 }; 102 };
93 wildcardMixin.call(this); 103 wildcardMixin.call(this);
94 if ( this.getEnumValues ) { 104 if ( this.getEnumValues ) {
95 restrictedChoicesMixin.call(this); 105 viewChoicesMixin.call(this);
96 }
97 var autocompletionUrl = utils.getMeta(this, 'autocompletionUrl');
98 if ( autocompletionUrl ) {
99 autoCompletionMixin.call(this, autocompletionUrl);
100 } 106 }
101 return this; 107 return this;
102 }); 108 });
@@ -114,19 +120,6 @@
114 } 120 }
115 }, {'@type': String}); 121 }, {'@type': String});
116 122
117 var autoCompletionMixin = Barricade.Blueprint.create(function(url) {
118 var self = this;
119
120 this.getSuggestions = function() { return []; };
121 $http.get(url).success(function(data) {
122 self.getSuggestions = function() {
123 return data;
124 };
125 });
126
127 return this;
128 });
129
130 var textModel = Barricade.Primitive.extend({ 123 var textModel = Barricade.Primitive.extend({
131 create: function(json, parameters) { 124 create: function(json, parameters) {
132 var self = Barricade.Primitive.create.call(this, json, parameters); 125 var self = Barricade.Primitive.create.call(this, json, parameters);
@@ -256,14 +249,55 @@
256 } 249 }
257 }, {'@type': Object}); 250 }, {'@type': Object});
258 251
252 var linkedCollectionModel = stringModel.extend({
253 create: function(json, parameters) {
254 var self = stringModel.create.call(this, json, parameters),
255 collectionCls = Barricade.create({
256 '@type': String,
257 '@ref': {
258 to: function() {
259 return parameters.toCls;
260 },
261 needs: function() {
262 return parameters.neededCls;
263 },
264 getter: function(data) {
265 return data.needed.get(parameters.substitutedEntryID);
266 }
267 }
268 });
269
270 self._collection = collectionCls.create().on(
271 'replace', function(newValue) {
272 self._collection = newValue;
273 self._collection.on('change', function() {
274 self._choices = self._collection.getIDs();
275 self.resetValues();
276 });
277 self._collection.emit('change');
278 });
279
280 return self;
281 },
282 _choices: []
283 }, {
284 '@enum': function() {
285 if ( this._collection.isPlaceholder() ) {
286 this.emit('_resolveUp', this._collection);
287 }
288 return this._choices;
289 }
290 }
291 );
292
259 return { 293 return {
260 string: stringModel, 294 string: stringModel,
261 text: textModel, 295 text: textModel,
262 number: numberModel, 296 number: numberModel,
263 list: listModel, 297 list: listModel,
298 linkedcollection: linkedCollectionModel,
264 dictionary: dictionaryModel, 299 dictionary: dictionaryModel,
265 frozendict: frozendictModel, 300 frozendict: frozendictModel,
266 autocompletionmixin: autoCompletionMixin,
267 wildcard: wildcardMixin // use for most general type-checks 301 wildcard: wildcardMixin // use for most general type-checks
268 }; 302 };
269 }]) 303 }])
diff --git a/merlin/static/merlin/templates/fields/choices.html b/merlin/static/merlin/templates/fields/choices.html
index c74e739..6f1b25f 100644
--- a/merlin/static/merlin/templates/fields/choices.html
+++ b/merlin/static/merlin/templates/fields/choices.html
@@ -1,9 +1,16 @@
1<div class="form-group"> 1<div class="form-group">
2 <label for="elem-{$ $id $}.$index">{$ value.title() $}</label> 2 <label for="elem-{$ $id $}">{$ value.title() $}</label>
3 <select id="elem-{$ $id $}.$index" class="form-control" 3 <select ng-if="value.isDropDown()"
4 id="elem-{$ $id $}" class="form-control"
4 ng-model="value.value" ng-model-options="{getterSetter: true}"> 5 ng-model="value.value" ng-model-options="{getterSetter: true}">
5 <option ng-repeat="option in value.getValues()" 6 <option ng-repeat="option in value.getValues()"
6 value="{$ option $}" 7 value="{$ option $}"
7 ng-selected="value.get() == option">{$ value.getLabel(option) $}</option> 8 ng-selected="value.get() == option">{$ value.getLabel(option) $}</option>
8 </select> 9 </select>
10 <input ng-if="!value.isDropDown()"
11 type="text" class="form-control" id="elem-{$ $id $}"
12 ng-model="value.value" ng-model-options="{ getterSetter: true }"
13 validatable-with="value" typeahead-editable="true"
14 typeahead="option for option in value.getValues() | filter:$viewValue">
15 <div ng-show="error" class="alert alert-danger">{$ error $}</div>
9</div> 16</div>
diff --git a/merlin/static/merlin/templates/fields/number.html b/merlin/static/merlin/templates/fields/number.html
index fbcc2b2..b0b10e1 100644
--- a/merlin/static/merlin/templates/fields/number.html
+++ b/merlin/static/merlin/templates/fields/number.html
@@ -3,6 +3,6 @@
3 <label for="elem-{$ $id $}">{$ value.title() $}</label> 3 <label for="elem-{$ $id $}">{$ value.title() $}</label>
4 <input type="number" class="form-control" id="elem-{$ $id $}" 4 <input type="number" class="form-control" id="elem-{$ $id $}"
5 ng-model="value.value" ng-model-options="{ getterSetter: true }" 5 ng-model="value.value" ng-model-options="{ getterSetter: true }"
6 autocompletable="true" validatable-with="value"> 6 validatable-with="value">
7 <div ng-show="error" class="alert alert-danger">{$ error $}</div> 7 <div ng-show="error" class="alert alert-danger">{$ error $}</div>
8</div> 8</div>
diff --git a/merlin/static/merlin/templates/fields/string.html b/merlin/static/merlin/templates/fields/string.html
index b526869..a6a1978 100644
--- a/merlin/static/merlin/templates/fields/string.html
+++ b/merlin/static/merlin/templates/fields/string.html
@@ -2,6 +2,6 @@
2 <label for="elem-{$ $id $}">{$ value.title() $}</label> 2 <label for="elem-{$ $id $}">{$ value.title() $}</label>
3 <input type="text" class="form-control" id="elem-{$ $id $}" 3 <input type="text" class="form-control" id="elem-{$ $id $}"
4 ng-model="value.value" ng-model-options="{ getterSetter: true }" 4 ng-model="value.value" ng-model-options="{ getterSetter: true }"
5 autocompletable="true" validatable-with="value"> 5 validatable-with="value">
6 <div ng-show="error" class="alert alert-danger">{$ error $}</div> 6 <div ng-show="error" class="alert alert-danger">{$ error $}</div>
7</div> 7</div>
diff --git a/merlin/static/merlin/templates/fields/text.html b/merlin/static/merlin/templates/fields/text.html
index 24173c3..f873ba7 100644
--- a/merlin/static/merlin/templates/fields/text.html
+++ b/merlin/static/merlin/templates/fields/text.html
@@ -2,6 +2,6 @@
2 <label for="elem-{$ $id $}">{$ value.title() $}</label> 2 <label for="elem-{$ $id $}">{$ value.title() $}</label>
3 <textarea class="form-control" id="elem-{$ $id $}" 3 <textarea class="form-control" id="elem-{$ $id $}"
4 ng-model="value.value" ng-model-options="{ getterSetter: true }" 4 ng-model="value.value" ng-model-options="{ getterSetter: true }"
5 autocompletable="true" validatable-with="value"></textarea> 5 validatable-with="value"></textarea>
6 <div ng-show="error" class="alert alert-danger">{$ error $}</div> 6 <div ng-show="error" class="alert alert-danger">{$ error $}</div>
7</div> 7</div>
diff --git a/merlin/test/js/modelsSpec.js b/merlin/test/js/modelsSpec.js
index cd0cbc0..db120c5 100644
--- a/merlin/test/js/modelsSpec.js
+++ b/merlin/test/js/modelsSpec.js
@@ -141,4 +141,65 @@ describe('merlin models:', function() {
141 }); 141 });
142 142
143 }); 143 });
144
145 describe('linkedCollection field', function() {
146 var collectionCls, linkedObjCls, linkedObj, lnkField;
147
148 beforeEach(function() {
149 collectionCls = fields.dictionary.extend({}, {
150 '?': {
151 '@class': fields.string
152 }
153 });
154 linkedObjCls = fields.frozendict.extend({}, {
155 'realCollection': {
156 '@class': collectionCls
157 },
158 'linkedField': {
159 '@class': fields.linkedcollection.extend({
160 create: function(json, parameters) {
161 parameters = Object.create(parameters);
162 parameters.toCls = collectionCls;
163 parameters.neededCls = linkedObjCls;
164 parameters.substitutedEntryID = 'realCollection';
165 return fields.linkedcollection.create.call(this, json, parameters);
166 },
167 _dropDownLimit: 4
168 })
169 }
170 });
171 linkedObj = linkedObjCls.create({'realCollection': {'a': '', 'b': ''}});
172 lnkField = linkedObj.get('linkedField');
173 });
174
175 it('provides access from @enum values of one field to IDs of another one', function() {
176 expect(lnkField.getValues()).toEqual(['a', 'b']);
177
178 linkedObj.get('realCollection').add('c');
179 expect(lnkField.getValues()).toEqual(['a', 'b', 'c']);
180 });
181
182 describe('and exposes _collection attribute', function() {
183 it('in case more complex things need to be done', function() {
184 expect(lnkField._collection).toBeDefined();
185 });
186
187 it("which is truly initialized after first @enum's .getValues() call", function() {
188 expect(lnkField._collection.isPlaceholder()).toBe(true);
189
190 lnkField.getValues();
191 expect(lnkField._collection.isPlaceholder()).toBe(false);
192 expect(lnkField._collection).toBe(linkedObj.get('realCollection'));
193 });
194 });
195
196 describe('exposes .isDropDown() call due to @enum presense', function() {
197 it('which always returns false due to deferred nature of linkedField', function() {
198 expect(lnkField.isDropDown()).toBe(false);
199
200 lnkField.getValues();
201 expect(lnkField.isDropDown()).toBe(false);
202 });
203 });
204 });
144}); \ No newline at end of file 205}); \ No newline at end of file