Browse Source

Merge "Refactor templates to make them composable"

changes/04/192704/11
Jenkins 3 years ago
parent
commit
ec9b892de4
29 changed files with 770 additions and 1236 deletions
  1. 2
    1
      bower.json
  2. 1
    1
      extensions/mistral/static/mistral/js/mistral.init.js
  3. 14
    0
      extensions/mistral/static/mistral/js/mistral.workbook.controllers.js
  4. 45
    67
      extensions/mistral/static/mistral/js/mistral.workbook.models.js
  5. 0
    91
      extensions/mistral/static/mistral/templates/fields/varlist.html
  6. 22
    0
      extensions/mistral/static/mistral/templates/fields/yaqlfield.html
  7. 0
    30
      extensions/mistral/static/mistral/templates/fields/yaqllist.html
  8. 33
    26
      extensions/mistral/templates/mistral/create.html
  9. 0
    7
      extensions/mistral/test/js/workbook.model.spec.js
  10. 30
    7
      merlin/static/merlin/js/merlin.directives.js
  11. 35
    39
      merlin/static/merlin/js/merlin.field.models.js
  12. 142
    103
      merlin/static/merlin/js/merlin.filters.js
  13. 1
    1
      merlin/static/merlin/js/merlin.init.js
  14. 15
    3
      merlin/static/merlin/js/merlin.utils.js
  15. 14
    85
      merlin/static/merlin/scss/merlin.scss
  16. 6
    6
      merlin/static/merlin/templates/collapsible-group.html
  17. 4
    2
      merlin/static/merlin/templates/collapsible-panel.html
  18. 13
    16
      merlin/static/merlin/templates/fields/choices.html
  19. 28
    13
      merlin/static/merlin/templates/fields/dictionary.html
  20. 21
    12
      merlin/static/merlin/templates/fields/frozendict.html
  21. 0
    13
      merlin/static/merlin/templates/fields/group.html
  22. 18
    11
      merlin/static/merlin/templates/fields/list.html
  23. 4
    8
      merlin/static/merlin/templates/fields/number.html
  24. 4
    7
      merlin/static/merlin/templates/fields/string.html
  25. 4
    7
      merlin/static/merlin/templates/fields/text.html
  26. 4
    0
      merlin/static/merlin/templates/labeled.html
  27. 1
    1
      merlin/test/js/merlin.directives.spec.js
  28. 305
    649
      merlin/test/js/merlin.filters.spec.js
  29. 4
    30
      merlin/test/js/merlin.models.spec.js

+ 2
- 1
bower.json View File

@@ -14,7 +14,8 @@
14 14
     "angular-moment": "0.9.0",
15 15
     "angular-cache": "3.2.5",
16 16
     "js-yaml": "3.2.7",
17
-    "underscore": "1.8.3"
17
+    "underscore": "1.8.3",
18
+    "flexboxgrid": "6.2.0"
18 19
   },
19 20
   "devDependencies": {
20 21
     "angular-mocks": "1.3.10",

+ 1
- 1
extensions/mistral/static/mistral/js/mistral.init.js View File

@@ -12,7 +12,7 @@
12 12
 
13 13
   function initModule(templates) {
14 14
     templates.prefetch('/static/mistral/templates/fields/',
15
-      ['varlist', 'yaqllist']);
15
+      ['yaqlfield']);
16 16
   }
17 17
 
18 18
 })();

+ 14
- 0
extensions/mistral/static/mistral/js/mistral.workbook.controllers.js View File

@@ -35,6 +35,20 @@
35 35
       });
36 36
     };
37 37
 
38
+    // Please see the explanation of how this determinant function works
39
+    // in the 'extractPanels' filter documentation
40
+    vm.keyExtractor = function(item, parent) {
41
+      if (item.instanceof(models.Action)) {
42
+        return 500 + parent.toArray().indexOf(item);
43
+      } else if (item.instanceof(models.Workflow)) {
44
+        return 1000 + parent.toArray().indexOf(item);
45
+      } else if (item.instanceof(Barricade.Container)) {
46
+        return null;
47
+      } else {
48
+        return 0;
49
+      }
50
+    };
51
+
38 52
     function getNextIDSuffix(container, regexp) {
39 53
       var max = Math.max.apply(Math, container.getIDs().map(function(id) {
40 54
         var match = regexp.exec(id);

+ 45
- 67
extensions/mistral/static/mistral/js/mistral.workbook.models.js View File

@@ -18,11 +18,15 @@
18 18
       if ( angular.isUndefined(json) || type === String ) {
19 19
         return fields.string.create(json, parameters);
20 20
       } else if ( type === Array ) {
21
-        return fields.list.extend({}, {
21
+        return fields.list.extend({
22
+          inline: true
23
+        }, {
22 24
           '*': {'@class': fields.string}
23 25
         }).create(json, parameters);
24 26
       } else if ( type === Object ) {
25
-        return fields.dictionary.extend({}, {
27
+        return fields.dictionary.extend({
28
+          inline: true
29
+        }, {
26 30
           '?': {'@class': fields.string}
27 31
         }).create(json, parameters);
28 32
       }
@@ -31,7 +35,6 @@
31 35
     models.varlist = fields.list.extend({
32 36
       create: function(json, parameters) {
33 37
         var self = fields.list.create.call(this, json, parameters);
34
-        self.setType('varlist');
35 38
         self.on('childChange', function(child, op) {
36 39
           if ( op == 'empty' ) {
37 40
             self.each(function(index, item) {
@@ -48,6 +51,7 @@
48 51
         '@class': fields.frozendict.extend({
49 52
           create: function(json, parameters) {
50 53
             var self = fields.frozendict.create.call(this, json, parameters);
54
+            self.isAtomic = function() { return false; };
51 55
             self.on('childChange', function(child) {
52 56
               if ( child.instanceof(Barricade.Enumerated) ) { // type change
53 57
                 var value = self.get('value');
@@ -87,25 +91,25 @@
87 91
       }
88 92
     });
89 93
 
90
-    models.yaqllist = fields.list.extend({
94
+    models.YAQLField = fields.frozendict.extend({
91 95
       create: function(json, parameters) {
92
-        var self = fields.list.create.call(this, json, parameters);
93
-        self.setType('yaqllist');
96
+        var self = fields.frozendict.create.call(this, json, parameters);
97
+        self.setType('yaqlfield');
94 98
         return self;
95 99
       }
96 100
     }, {
97
-      '*': {
98
-        '@class': fields.frozendict.extend({}, {
99
-          'yaql': {
100
-            '@class': fields.string
101
-          },
102
-          'action': {
103
-            '@class': fields.string
104
-          }
105
-        })
101
+      'yaql': {
102
+        '@class': fields.string
103
+      },
104
+      'action': {
105
+        '@class': fields.string
106 106
       }
107 107
     });
108 108
 
109
+    models.yaqllist = fields.list.extend({}, {
110
+      '*': {'@class': models.YAQLField}
111
+    });
112
+
109 113
     models.Action =  fields.frozendict.extend({
110 114
       create: function(json, parameters) {
111 115
         var self = fields.frozendict.create.call(this, json, parameters);
@@ -135,8 +139,7 @@
135 139
           }
136 140
         }, {
137 141
           '@meta': {
138
-            'index': 1,
139
-            'row': 0
142
+            'index': 1
140 143
           }
141 144
         })
142 145
       },
@@ -144,18 +147,24 @@
144 147
         '@class': fields.dictionary.extend({
145 148
           create: function(json, parameters) {
146 149
             var self = fields.dictionary.create.call(this, json, parameters);
150
+            self.isAdditive = function() { return false; };
147 151
             self.setType('frozendict');
148 152
             return self;
153
+          },
154
+          // here we override `each' method inherited from fields.dictionary<-MutableObject
155
+          // because it provides entry index as the first argument of the callback, while
156
+          // we need to get the key/ID value as first argument (mimicking the `each' method
157
+          // ImmutableObject)
158
+          each: function(callback) {
159
+            var self = this;
160
+            this.getIDs().forEach(function(id) {
161
+              callback.call(self, id, self.getByID(id));
162
+            });
163
+            return this;
149 164
           }
150 165
         }, {
151 166
           '@required': false,
152
-          '?': {
153
-            '@class': fields.string.extend({}, {
154
-              '@meta': {
155
-                'row': 0
156
-              }
157
-            })
158
-          },
167
+          '?': {'@class': fields.string},
159 168
           '@meta': {
160 169
             'index': 2,
161 170
             'title': 'Base Input'
@@ -189,9 +198,6 @@
189 198
         });
190 199
         return self;
191 200
       },
192
-      remove: function() {
193
-        this.emit('change', 'taskRemove', this.getID());
194
-      },
195 201
       _getPrettyJSON: function() {
196 202
         var json = fields.frozendict._getPrettyJSON.apply(this, arguments);
197 203
         delete json.type;
@@ -200,10 +206,7 @@
200 206
     }, {
201 207
       '@meta': {
202 208
         'baseKey': 'task',
203
-        'baseName': 'Task ',
204
-        'group': true,
205
-        'additive': false,
206
-        'removable': true
209
+        'baseName': 'Task '
207 210
       },
208 211
       'type': {
209 212
         '@class': fields.string.extend({}, {
@@ -214,16 +217,14 @@
214 217
           }],
215 218
           '@default': 'action',
216 219
           '@meta': {
217
-            'index': 0,
218
-            'row': 0
220
+            'index': 0
219 221
           }
220 222
         })
221 223
       },
222 224
       'description': {
223 225
         '@class': fields.text.extend({}, {
224 226
           '@meta': {
225
-            'index': 2,
226
-            'row': 1
227
+            'index': 2
227 228
           }
228 229
         })
229 230
       },
@@ -268,7 +269,6 @@
268 269
               '@required': false,
269 270
               '@meta': {
270 271
                 'index': 0,
271
-                'row': 0,
272 272
                 'title': 'Wait before'
273 273
               }
274 274
             })
@@ -278,7 +278,6 @@
278 278
               '@required': false,
279 279
               '@meta': {
280 280
                 'index': 1,
281
-                'row': 0,
282 281
                 'title': 'Wait after'
283 282
               }
284 283
             })
@@ -287,8 +286,7 @@
287 286
             '@class': fields.number.extend({}, {
288 287
               '@required': false,
289 288
               '@meta': {
290
-                'index': 2,
291
-                'row': 1
289
+                'index': 2
292 290
               }
293 291
             })
294 292
           },
@@ -297,7 +295,6 @@
297 295
               '@required': false,
298 296
               '@meta': {
299 297
                 'index': 3,
300
-                'row': 2,
301 298
                 'title': 'Retry count'
302 299
               }
303 300
             })
@@ -307,7 +304,6 @@
307 304
               '@required': false,
308 305
               '@meta': {
309 306
                 'index': 4,
310
-                'row': 2,
311 307
                 'title': 'Retry delay'
312 308
               }
313 309
             })
@@ -317,7 +313,6 @@
317 313
               '@required': false,
318 314
               '@meta': {
319 315
                 'index': 5,
320
-                'row': 3,
321 316
                 'title': 'Retry break on'
322 317
               }
323 318
             })
@@ -330,7 +325,6 @@
330 325
       'requires': {
331 326
         '@class': fields.string.extend({}, {
332 327
           '@meta': {
333
-            'row': 2,
334 328
             'index': 3
335 329
           }
336 330
         })
@@ -386,7 +380,6 @@
386 380
             }
387 381
           }, {
388 382
             '@meta': {
389
-              'row': 0,
390 383
               'index': 1
391 384
             }
392 385
           })
@@ -407,7 +400,6 @@
407 400
             }
408 401
           }, {
409 402
             '@meta': {
410
-              'row': 0,
411 403
               'index': 1
412 404
             }
413 405
           })
@@ -446,8 +438,7 @@
446 438
           '@enum': ['reverse', 'direct'],
447 439
           '@default': 'direct',
448 440
           '@meta': {
449
-            'index': 1,
450
-            'row': 0
441
+            'index': 1
451 442
           }
452 443
         })
453 444
       },
@@ -485,16 +476,13 @@
485 476
                 var taskData = child.toJSON();
486 477
                 params.id = taskId;
487 478
                 self.set(taskPos, TaskFactory(taskData, params));
488
-              } else if ( op === 'taskRemove' ) {
489
-                self.removeItem(arg);
490 479
               }
491 480
             });
492 481
             return self;
493 482
           }
494 483
         }, {
495 484
           '@meta': {
496
-            'index': 5,
497
-            'group': true
485
+            'index': 5
498 486
           },
499 487
           '?': {
500 488
             '@class': models.Task,
@@ -511,9 +499,7 @@
511 499
         '@class': fields.frozendict.extend({}, {
512 500
           '@required': false,
513 501
           '@meta': {
514
-            'index': 4,
515
-            'group': true,
516
-            'additive': false
502
+            'index': 4
517 503
           },
518 504
           'on-error': {
519 505
             '@class': models.yaqllist.extend({}, {
@@ -557,8 +543,7 @@
557 543
     models.Actions = fields.dictionary.extend({}, {
558 544
       '@required': false,
559 545
       '@meta': {
560
-        'index': 3,
561
-        'panelIndex': 1
546
+        'index': 3
562 547
       },
563 548
       '?': {
564 549
         '@class': models.Action
@@ -583,8 +568,7 @@
583 568
       }
584 569
     }, {
585 570
       '@meta': {
586
-        'index': 4,
587
-        'panelIndex': 2
571
+        'index': 4
588 572
       },
589 573
       '?': {
590 574
         '@class': models.Workflow,
@@ -601,9 +585,7 @@
601 585
         '@class': fields.string.extend({}, {
602 586
           '@enum': ['2.0'],
603 587
           '@meta': {
604
-            'index': 2,
605
-            'panelIndex': 0,
606
-            'row': 1
588
+            'index': 2
607 589
           },
608 590
           '@default': '2.0'
609 591
         })
@@ -611,9 +593,7 @@
611 593
       'name': {
612 594
         '@class': fields.string.extend({}, {
613 595
           '@meta': {
614
-            'index': 0,
615
-            'panelIndex': 0,
616
-            'row': 0
596
+            'index': 0
617 597
           },
618 598
           '@constraints': [
619 599
             function(value) {
@@ -625,9 +605,7 @@
625 605
       'description': {
626 606
         '@class': fields.text.extend({}, {
627 607
           '@meta': {
628
-            'index': 1,
629
-            'panelIndex': 0,
630
-            'row': 0
608
+            'index': 1
631 609
           },
632 610
           '@required': false
633 611
         })

+ 0
- 91
extensions/mistral/static/mistral/templates/fields/varlist.html View File

@@ -1,91 +0,0 @@
1
-<collapsible-group content="value"
2
-                   on-add="value.add()">
3
-  <div class="three-columns" ng-repeat="subItem in value.getValues() track by $index"
4
-       ng-class="subItem.get('type').get()">
5
-    <div class="left-column">
6
-      <div class="form-group">
7
-        <label for="elem-{$ $id $}.$index">Key Type</label>
8
-        <select id="elem-{$ $id $}.$index" class="form-control"
9
-                ng-model="subItem.get('type').value" ng-model-options="{getterSetter: true}">
10
-          <option ng-repeat="value in subItem.get('type').getEnumValues()"
11
-                  value="{$ value $}"
12
-                  ng-selected="subItem.get('type').get() == value">{$ value $}</option>
13
-        </select>
14
-      </div>
15
-    </div>
16
-    <div ng-switch="subItem.get('type').value()">
17
-      <!-- draw string input -->
18
-      <div class="right-column" ng-switch-when="string">
19
-        <div class="form-group">
20
-          <label>&nbsp;</label>
21
-          <div class="input-group">
22
-            <input type="text" class="form-control"
23
-                   ng-model="subItem.get('value').value" ng-model-options="{getterSetter: true}">
24
-            <span class="input-group-btn">
25
-              <button class="btn btn-default" ng-click="value.remove($index)">
26
-                <i class="fa fa-minus-circle"></i>
27
-              </button>
28
-            </span>
29
-          </div>
30
-        </div>
31
-      </div>
32
-      <!-- END: draw string input -->
33
-      <!-- draw dictionary inputs -->
34
-      <div ng-switch-when="dictionary">
35
-        <div ng-repeat="(key, value) in subItem.get('value').getValues() track by key">
36
-          <div ng-hide="$first" class="left-column"></div>
37
-          <div class="right-column">
38
-            <div class="form-group">
39
-              <label for="elem-{$ $id $}.{$ key $}">
40
-                <editable ng-model="value.keyValue" ng-model-options="{getterSetter: true}"></editable>
41
-              </label>
42
-              <div class="input-group">
43
-                <input type="text" id="elem-{$ $id $}.{$ key $}" class="form-control" ng-model="value.value"
44
-                       ng-model-options="{getterSetter: true}">
45
-                <span class="input-group-btn">
46
-                  <button class="btn btn-default" ng-click="subItem.get('value').remove(key)">
47
-                    <i class="fa fa-minus-circle"></i>
48
-                  </button>
49
-                </span>
50
-              </div>
51
-            </div>
52
-          </div>
53
-          <div ng-hide="$last" class="clearfix"></div>
54
-          <div ng-show="$last" class="add-btn button-column">
55
-            <button class="btn btn-default btn-sm pull-right" ng-click="subItem.get('value').add()">
56
-              <i class="fa fa-plus"></i>
57
-            </button>
58
-          </div>
59
-        </div>
60
-      </div>
61
-      <!-- END: draw dictionary inputs -->
62
-      <!-- draw list inputs -->
63
-      <div ng-switch-when="list">
64
-        <div ng-repeat="value in subItem.get('value').getValues() track by $index">
65
-          <div ng-hide="$first" class="left-column"></div>
66
-          <div class="right-column">
67
-            <div class="form-group">
68
-              <label ng-show="$first">&nbsp;</label>
69
-              <div class="input-group">
70
-                <input type="text" class="form-control" ng-model="value.value"
71
-                       ng-model-options="{getterSetter: true}">
72
-                <span class="input-group-btn">
73
-                  <button class="btn btn-default" ng-click="subItem.get('value').remove($index)">
74
-                    <i class="fa fa-minus-circle"></i>
75
-                  </button>
76
-                </span>
77
-              </div>
78
-            </div>
79
-          </div>
80
-          <div ng-hide="$last" class="clearfix"></div>
81
-          <div ng-show="$last" class="add-btn button-column" ng-class="{'varlist-1st-row': !$index}">
82
-            <button class="btn btn-default btn-sm pull-right" ng-click="subItem.get('value').add()">
83
-              <i class="fa fa-plus"></i>
84
-            </button>
85
-          </div>
86
-        </div>
87
-      </div>
88
-      <!-- END: draw list inputs -->
89
-    </div>
90
-  </div>
91
-</collapsible-group>

+ 22
- 0
extensions/mistral/static/mistral/templates/fields/yaqlfield.html View File

@@ -0,0 +1,22 @@
1
+<div class="row">
2
+  <div class="col-xs" ng-show="value.showYaql">
3
+    <div class="form-group">
4
+      <textarea class="form-control" ng-model="value.get('yaql').value"
5
+                ng-model-options="{getterSetter: true}"></textarea>
6
+    </div>
7
+  </div>
8
+  <div class="col-xs-6">
9
+    <div class="form-group">
10
+      <div class="input-group">
11
+          <span class="input-group-btn">
12
+            <button class="btn btn-default" ng-click="value.showYaql = !value.showYaql;">
13
+              <i class="fa"
14
+                 ng-class="{'fa-lock': value.get('yaql').value(), 'fa-unlock': !value.get('yaql').value()}"></i>
15
+            </button>
16
+          </span>
17
+        <input type="text" class="form-control" ng-model="value.get('action').value"
18
+               ng-model-options="{getterSetter: true}">
19
+      </div>
20
+    </div>
21
+  </div>
22
+</div>

+ 0
- 30
extensions/mistral/static/mistral/templates/fields/yaqllist.html View File

@@ -1,30 +0,0 @@
1
-<collapsible-group content="value" on-add="value.add()">
2
-  <div class="three-columns"
3
-       ng-repeat="subItem in value.getValues() track by $index">
4
-    <div class="left-column" ng-show="subItem.showYaql">
5
-      <div class="form-group">
6
-        <textarea class="form-control" ng-model="subItem.get('yaql').value"
7
-                  ng-model-options="{getterSetter: true}"></textarea>
8
-      </div>
9
-    </div>
10
-    <div class="right-column">
11
-      <div class="form-group">
12
-        <div class="input-group">
13
-          <span class="input-group-btn">
14
-            <button class="btn btn-default" ng-click="subItem.showYaql = !subItem.showYaql;">
15
-              <i class="fa"
16
-                 ng-class="{'fa-lock': subItem.get('yaql').value(), 'fa-unlock': !subItem.get('yaql').value()}"></i>
17
-            </button>
18
-          </span>
19
-          <input type="text" class="form-control" ng-model="subItem.get('action').value"
20
-                 ng-model-options="{getterSetter: true}">
21
-          <span class="input-group-btn">
22
-            <button class="btn btn-default" ng-click="value.remove($index)">
23
-              <i class="fa fa-minus-circle"></i>
24
-            </button>
25
-          </span>
26
-        </div>
27
-      </div>
28
-    </div>
29
-  </div>
30
-</collapsible-group>

+ 33
- 26
extensions/mistral/templates/mistral/create.html View File

@@ -33,21 +33,22 @@
33 33
   {% compress css %}
34 34
     <link href='{{ STATIC_URL }}merlin/scss/merlin.scss' type='text/scss' media='screen' rel='stylesheet' />
35 35
   {% endcompress %}
36
+<link href='{{ STATIC_URL }}merlin/libs/flexboxgrid/dist/flexboxgrid.css' type='text/css' media='screen' rel='stylesheet' />
36 37
   {% block merlin-css %}{% endblock %}
37 38
 {% endblock %}
38 39
 
39 40
 {% block main %}
40 41
 <h3>Create Workbook</h3>
41
-  <div id="create-workbook" class="fluid-container" ng-cloak ng-controller="WorkbookController as wb"
42
+  <div id="create-workbook" ng-cloak ng-controller="WorkbookController as wb"
42 43
        ng-init="wb.init({{ id|default:'undefined' }}, '{{ yaml }}', '{{ commit_url }}', '{{ discard_url }}')">
43 44
     <div class="well">
44
-      <div class="two-panels">
45
-        <div class="left-panel">
46
-          <div class="pull-left">
45
+      <div class="row">
46
+        <div class="col-xs row">
47
+          <div class="col-xs start-xs">
47 48
             <h4><strong>{$ wb.workbook.get('name') $}</strong></h4>
48 49
           </div>
49
-          <div class="pull-right">
50
-            <div class="table-actions clearfix">
50
+          <div class="col-xs end-xs">
51
+            <div class="table-actions">
51 52
               <button ng-click="wb.addAction()" class="btn btn-default btn-sm">
52 53
                 <span class="fa fa-plus">Add Action</span></button>
53 54
               <button ng-click="wb.addWorkflow()" class="btn btn-default btn-sm">
@@ -55,8 +56,8 @@
55 56
             </div>
56 57
           </div>
57 58
         </div>
58
-        <div class="right-panel">
59
-          <div class="btn-group btn-toggle pull-right">
59
+        <div class="col-xs end-xs">
60
+          <div class="btn-group btn-toggle">
60 61
             <button ng-click="wb.isGraphMode = true" class="btn btn-sm"
61 62
                 ng-class="wb.isGraphMode ? 'active btn-primary' : 'btn-default'">Graph</button>
62 63
             <button ng-click="wb.isGraphMode = false" class="btn btn-sm"
@@ -65,23 +66,31 @@
65 66
         </div>
66 67
       </div>
67 68
       <!-- Data panel start -->
68
-      <div class="two-panels">
69
-        <div class="left-panel">
70
-          <panel ng-repeat="panel in wb.workbook | extractPanels track by panel.id"
69
+      <div class="row">
70
+        <div class="col-xs">
71
+          <panel ng-repeat="panel in wb.workbook | extractPanels:wb.keyExtractor track by panel.id"
71 72
                  content="panel">
72
-            <div ng-repeat="row in panel | extractRows track by row.id">
73
-              <div ng-class="{'two-columns': row.index !== undefined }">
74
-                <div ng-repeat="item in row | extractItems track by item.id"
75
-                     ng-class="{'right-column': item.isAtomic() && $odd, 'left-column': item.isAtomic() && $even}">
76
-                  <typed-field value="item" type="{$ item.getType() $}"></typed-field>
77
-                  <div class="clearfix" ng-if="$odd"></div>
73
+            <div ng-repeat="row in panel | extractFields | chunks:2 track by $index">
74
+              <div ng-repeat="(label, field) in row track by field.uid()">
75
+                <div ng-if="field.isAtomic()" class="col-xs-6">
76
+                  <labeled label="{$ label $}" for="{$ field.uid() $}">
77
+                    <typed-field value="field" type="{$ field.getType() $}"></typed-field>
78
+                  </labeled>
79
+                </div>
80
+                <div ng-if="!field.isAtomic()" class="col-xs-12">
81
+                  <collapsible-group content="field" title="label"
82
+                                     additive="{$ field.isAdditive() $}" on-add="field.add()">
83
+                    <div ng-class="field.isPlainStructure() ? 'col-xs-6' : 'col-xs-12'">
84
+                      <typed-field value="field" type="{$ field.getType() $}"></typed-field>
85
+                    </div>
86
+                  </collapsible-group>
78 87
                 </div>
79 88
               </div>
80 89
             </div>
81 90
           </panel>
82 91
         </div>
83 92
         <!-- YAML Panel -->
84
-        <div class="right-panel">
93
+        <div class="col-xs">
85 94
           <div class="panel panel-default">
86 95
             <div class="panel-body" ng-show="!wb.isGraphMode">
87 96
               <pre>{$ wb.workbook.toYAML() $}</pre>
@@ -93,14 +102,12 @@
93 102
         </div>
94 103
       </div>
95 104
       <!-- page footer -->
96
-      <div class="two-panels">
97
-        <div class="full-width">
98
-          <div class="pull-right">
99
-            <button ng-click="wb.discardWorkbook()" class="btn btn-default cancel">Cancel</button>
100
-            <button ng-click="wb.commitWorkbook()" class="btn btn-primary">
101
-              {$ wb.workbookID ? 'Modify' : 'Create' $}
102
-              </button>
103
-          </div>
105
+      <div class="row">
106
+        <div class="col-xs end-xs">
107
+          <button ng-click="wb.discardWorkbook()" class="btn btn-default cancel">Cancel</button>
108
+          <button ng-click="wb.commitWorkbook()" class="btn btn-primary">
109
+            {$ wb.workbookID ? 'Modify' : 'Create' $}
110
+          </button>
104 111
         </div>
105 112
       </div>
106 113
     </div>

+ 0
- 7
extensions/mistral/test/js/workbook.model.spec.js View File

@@ -129,13 +129,6 @@ describe('workbook model logic', function() {
129 129
         expect(json.workflows[workflowID].tasks[newID]).toBeDefined();
130 130
       });
131 131
 
132
-      it('a task deletion works in conjunction with tasks logic', function() {
133
-        expect(getTask(taskID)).toBeDefined();
134
-
135
-        getTask(taskID).remove();
136
-        expect(getTask(taskID)).toBeUndefined();
137
-      });
138
-
139 132
     });
140 133
 
141 134
     describe("which start with the 'direct' workflow:", function() {

+ 30
- 7
merlin/static/merlin/js/merlin.directives.js View File

@@ -39,9 +39,21 @@
39 39
      * retrieves a template by its name which is the same as model's type and renders it,
40 40
      * recursive <typed-field></..>-s are possible.
41 41
      * */
42
-    .directive('typedField', typedField);
42
+    .directive('typedField', typedField)
43 43
 
44
-  typedField.$inject = ['$compile', 'merlin.templates'];
44
+    .directive('labeled', labeled);
45
+
46
+  function labeled() {
47
+    return {
48
+      restrict: 'E',
49
+      templateUrl: '/static/merlin/templates/labeled.html',
50
+      transclude: true,
51
+      scope: {
52
+        label: '@',
53
+        for: '@'
54
+      }
55
+    };
56
+  }
45 57
 
46 58
   function editable() {
47 59
     return {
@@ -100,6 +112,7 @@
100 112
     };
101 113
   }
102 114
 
115
+  showFocus.$inject = ['$timeout'];
103 116
   function showFocus($timeout) {
104 117
     return function(scope, element, attrs) {
105 118
       // Unused variable created here due to rule 'ng_on_watch': 2
@@ -114,7 +127,7 @@
114 127
     };
115 128
   }
116 129
 
117
-  function panel($parse) {
130
+  function panel() {
118 131
     return {
119 132
       restrict: 'E',
120 133
       templateUrl: '/static/merlin/templates/collapsible-panel.html',
@@ -122,9 +135,13 @@
122 135
       scope: {
123 136
         panel: '=content'
124 137
       },
125
-      link: function(scope, element, attrs) {
126
-        scope.removable = $parse(attrs.removable)();
127
-        scope.isCollapsed = false;
138
+      link: function(scope) {
139
+        if (angular.isDefined(scope.panel)) {
140
+          scope.isCollapsed = false;
141
+          if (angular.isFunction(scope.panel.title)) {
142
+            scope.editable = true;
143
+          }
144
+        }
128 145
       }
129 146
     };
130 147
   }
@@ -136,11 +153,15 @@
136 153
       transclude: true,
137 154
       scope: {
138 155
         group: '=content',
156
+        title: '=',
139 157
         onAdd: '&',
140 158
         onRemove: '&'
141 159
       },
142 160
       link: function(scope, element, attrs) {
143 161
         scope.isCollapsed = false;
162
+        if (angular.isFunction(scope.title)) {
163
+          scope.editable = true;
164
+        }
144 165
         if ( attrs.onAdd && attrs.additive !== 'false' ) {
145 166
           scope.additive = true;
146 167
         }
@@ -151,6 +172,7 @@
151 172
     };
152 173
   }
153 174
 
175
+  validatableWith.$inject = ['$parse'];
154 176
   function validatableWith($parse) {
155 177
     return {
156 178
       restrict: 'A',
@@ -186,6 +208,7 @@
186 208
     };
187 209
   }
188 210
 
211
+  typedField.$inject = ['$compile', 'merlin.templates'];
189 212
   function typedField($compile, templates) {
190 213
     return {
191 214
       restrict: 'E',
@@ -195,7 +218,7 @@
195 218
       },
196 219
       link: function(scope, element) {
197 220
         templates.templateReady(scope.type).then(function(template) {
198
-          element.replaceWith($compile(template)(scope));
221
+          element.append($compile(template)(scope));
199 222
         });
200 223
       }
201 224
     };

+ 35
- 39
merlin/static/merlin/js/merlin.field.models.js View File

@@ -61,6 +61,24 @@
61 61
       return this;
62 62
     });
63 63
 
64
+    /* Html renderer helper. The main idea is that fields with simple (or plain)
65
+    structure (i.e. Atomics = string | number | text | boolean and list or
66
+    dictionary containing just Atomics) could be rendered in one column, while
67
+    fields with non plain structure should be rendered in two columns.
68
+     */
69
+    var plainStructureMixin = Barricade.Blueprint.create(function() {
70
+      this.isPlainStructure = function() {
71
+        if (this.getType() == 'frozendict') {
72
+          return false;
73
+        }
74
+        if (!this.instanceof(Barricade.Arraylike) || !this.length()) {
75
+          return false;
76
+        }
77
+        return !this.get(0).instanceof(Barricade.Container);
78
+      };
79
+      return this;
80
+    });
81
+
64 82
     var modelMixin = Barricade.Blueprint.create(function(type) {
65 83
       var isValid = true;
66 84
       var isValidatable = false;
@@ -90,8 +108,12 @@
90 108
         type = _type;
91 109
       };
92 110
 
111
+      this.isAdditive = function() {
112
+        return this.instanceof(Barricade.Arraylike);
113
+      };
114
+
93 115
       this.isAtomic = function() {
94
-        return ['number', 'string', 'text', 'choices'].indexOf(this.getType()) > -1;
116
+        return !this.instanceof(Barricade.Container);
95 117
       };
96 118
       this.title = function() {
97 119
         var title = utils.getMeta(this, 'title');
@@ -148,13 +170,8 @@
148 170
         self.add = function() {
149 171
           self.push(undefined, parameters);
150 172
         };
151
-        self.getValues = function() {
152
-          return self.toArray();
153
-        };
154
-        self._getContents = function() {
155
-          return self.toArray();
156
-        };
157 173
         meldGroup.call(self);
174
+        plainStructureMixin.call(self);
158 175
         return self;
159 176
       }
160 177
     }, {'@type': Array});
@@ -162,20 +179,10 @@
162 179
     var frozendictModel = Barricade.ImmutableObject.extend({
163 180
       create: function(json, parameters) {
164 181
         var self = Barricade.ImmutableObject.create.call(this, json, parameters);
165
-        self.getKeys().forEach(function(key) {
166
-          utils.enhanceItemWithID(self.get(key), key);
167
-        });
168 182
 
169 183
         modelMixin.call(self, 'frozendict');
170
-        self.getValues = function() {
171
-          return self._data;
172
-        };
173
-        self._getContents = function() {
174
-          return self.getKeys().map(function(key) {
175
-            return self.get(key);
176
-          });
177
-        };
178 184
         meldGroup.call(self);
185
+        plainStructureMixin.call(self);
179 186
         return self;
180 187
       }
181 188
     }, {'@type': Object});
@@ -183,15 +190,14 @@
183 190
     var dictionaryModel = Barricade.MutableObject.extend({
184 191
       create: function(json, parameters) {
185 192
         var self = Barricade.MutableObject.create.call(this, json, parameters);
186
-        var _items = [];
187 193
         var _elClass = self._elementClass;
188 194
         var baseKey = utils.getMeta(_elClass, 'baseKey') || 'key';
189 195
         var baseName = utils.getMeta(_elClass, 'baseName') || utils.makeTitle(baseKey);
190 196
 
191 197
         modelMixin.call(self, 'dictionary');
198
+        plainStructureMixin.call(self);
192 199
 
193
-        function makeCacheWrapper(container, key) {
194
-          var value = container.getByID(key);
200
+        function initKeyAccessor(value) {
195 201
           value.keyValue = function () {
196 202
             if ( arguments.length ) {
197 203
               value.setID(arguments[0]);
@@ -199,9 +205,16 @@
199 205
               return value.getID();
200 206
             }
201 207
           };
202
-          return value;
203 208
         }
204 209
 
210
+        self.each(function(key, value) {
211
+          initKeyAccessor(value);
212
+        }).on('change', function(op, index) {
213
+          if (op === 'add' || op === 'set') {
214
+            initKeyAccessor(self.get(index));
215
+          }
216
+        });
217
+
205 218
         self.add = function(newID) {
206 219
           var regexp = new RegExp('(' + baseKey + ')([0-9]+)');
207 220
           var newValue;
@@ -217,21 +230,11 @@
217 230
             newValue = '';
218 231
           }
219 232
           self.push(newValue, utils.extend(self._parameters, {id: newID}));
220
-          _items.push(makeCacheWrapper(self, newID));
221
-        };
222
-        self.getValues = function() {
223
-          if ( !_items.length ) {
224
-            _items = self.toArray().map(function(value) {
225
-              return makeCacheWrapper(self, value.getID());
226
-            });
227
-          }
228
-          return _items;
229 233
         };
230 234
         self.empty = function() {
231 235
           for ( var i = this._data.length; i > 0; i-- ) {
232 236
             self.remove(i - 1);
233 237
           }
234
-          _items = [];
235 238
         };
236 239
         self.resetKeys = function(keys) {
237 240
           self.empty();
@@ -239,17 +242,10 @@
239 242
             self.push(undefined, {id: key});
240 243
           });
241 244
         };
242
-        self._getContents = function() {
243
-          return self.toArray();
244
-        };
245 245
         self.removeItem = function(key) {
246
-          var pos = self.getPosByID(key);
247 246
           self.remove(self.getPosByID(key));
248
-          _items.splice(pos, 1);
249 247
         };
250 248
         meldGroup.call(self);
251
-        // initialize cache with starting values
252
-        self.getValues();
253 249
         return self;
254 250
       }
255 251
     }, {'@type': Object});

+ 142
- 103
merlin/static/merlin/js/merlin.filters.js View File

@@ -16,148 +16,187 @@
16 16
 (function() {
17 17
   angular
18 18
     .module('merlin')
19
+    /* 'extractPanels' filter requires one argument which should be a function.
20
+    This function is applied to the top-level elements of the object and the
21
+    fields for which it returns a numeric value are grouped into the panels. More
22
+    precisely, each field yielding the same numeric value is put into the same panel.
23
+    Subclasses of Barricade.Container which don't yield a numeric value (and return
24
+    null, for example) become the entry points of a recursive application of above
25
+    algorithm, so eventually each field will be either:
26
+    * put into a panel (determinant returns numeric value)
27
+    * recursively scanned for more fields (is a container, no numeric value returned)
28
+    * or skipped completely (neither of above conditions is met).
29
+
30
+    Each returned panel implements at least .each() method (iterating through all key &
31
+    field pairs of a panel) which could be later consumed by 'extractFields' filter.
32
+    Filter results are cached, with each field explicitly put into a panel by determinant
33
+    (i.e. yielding a numeric value) adds its unique id to the caching key. This means that
34
+    the filter returns a new set of panels if the set of fields explicitly put into panels
35
+    changes - i.e. a value goes away or comes in into a set or replaced in place with
36
+    another value (any case is tracked by the unique field id).
37
+     */
19 38
     .filter('extractPanels', extractPanels)
20
-    .filter('extractRows', extractRows)
21
-    .filter('extractItems', extractItems);
39
+    .filter('extractFields', extractFields)
40
+    .filter('chunks', chunks);
22 41
 
23 42
   extractPanels.$inject = ['merlin.utils'];
24
-  extractRows.$inject = ['merlin.utils'];
25
-  extractItems.$inject = ['merlin.utils'];
26 43
 
27 44
   function extractPanels(utils) {
28 45
     var panelProto = {
29
-      create: function(itemsOrContainer, id) {
30
-        if ( angular.isArray(itemsOrContainer) && !itemsOrContainer.length ) {
31
-          return null;
32
-        }
33
-        if ( angular.isArray(itemsOrContainer) ) {
34
-          this.items = itemsOrContainer;
35
-          this.id = itemsOrContainer.reduce(function(prevId, item) {
36
-            return item.uid() + prevId;
37
-          }, '');
46
+      create: function(enumerator, obj, context) {
47
+        this.$$obj = obj;
48
+        this.$$enumerator = enumerator;
49
+        this.removable = false;
50
+        if (this.$$obj) {
51
+          this.id = this.$$obj.uid();
52
+          this.$$objParent = context.container;
53
+          this.removable = this.$$objParent.instanceof(Barricade.Arraylike);
54
+          if (this.$$objParent.instanceof(Barricade.MutableObject)) {
55
+            this.title = function() {
56
+              if ( arguments.length ) {
57
+                obj.setID(arguments[0]);
58
+              } else {
59
+                return obj.getID();
60
+              }
61
+            };
62
+          } else if (this.$$objParent.instanceof(Barricade.ImmutableObject)) {
63
+            this.title = context.indexOrKey;
64
+          }
38 65
         } else {
39
-          this._barricadeContainer = itemsOrContainer;
40
-          this._barricadeId = id;
41
-          var barricadeObj = itemsOrContainer.getByID(id);
42
-          this.id = barricadeObj.uid();
43
-          this.items = barricadeObj.getKeys().map(function(key) {
44
-            return utils.enhanceItemWithID(barricadeObj.get(key), key);
66
+          var id = '';
67
+          this.$$enumerator(function(key, item) {
68
+            id += item.uid();
45 69
           });
46
-          this.removable = true;
70
+          this.id = id;
47 71
         }
48 72
         return this;
49 73
       },
50
-      title: function() {
51
-        var newID;
52
-        if ( this._barricadeContainer ) {
53
-          if ( arguments.length ) {
54
-            newID = arguments[0];
55
-            this._barricadeContainer.getByID(this._barricadeId).setID(newID);
56
-            this._barricadeId = newID;
57
-          } else {
58
-            return this._barricadeId;
59
-          }
60
-        }
74
+      each: function(callback, comparator) {
75
+        this.$$enumerator.call(this.$$obj, callback, comparator);
61 76
       },
62 77
       remove: function() {
63
-        var container = this._barricadeContainer;
64
-        var pos = container.getPosByID(this._barricadeId);
65
-        container.remove(pos);
78
+        var index;
79
+        if (this.removable) {
80
+          index = this.$$objParent.toArray().indexOf(this.$$obj);
81
+          this.$$objParent.remove(index);
82
+        }
66 83
       }
67 84
     };
68 85
 
69
-    function isPanelsRoot(item) {
70
-      try {
71
-        // check for 'actions' and 'workflows' containers
72
-        return item.instanceof(Barricade.MutableObject);
73
-      }
74
-      catch(err) {
75
-        return false;
86
+    return _.memoize(function(container, keyExtractor) {
87
+      var items = [];
88
+      var _data = {};
89
+      var panels = [];
90
+
91
+      /* This function recursively applies determinant 'keyExtractor' function
92
+      to each container (given that the determinant doesn't return a numeric
93
+      value for it), starting from the top-level. Fields for which determinant
94
+      returns a numeric value, will be later placed into a panels (see docs for
95
+      'extractPanels' filter).
96
+      */
97
+      function rec(container) {
98
+        container.each(function(indexOrKey, item) {
99
+          var groupingKey = keyExtractor(item, container);
100
+          if (angular.isNumber(groupingKey)) {
101
+            items.push(item);
102
+            _data[item.uid()] = {
103
+              groupingKey: groupingKey,
104
+              container: container,
105
+              indexOrKey: indexOrKey
106
+            };
107
+          } else if (item.instanceof(Barricade.Container)) {
108
+            rec(item);
109
+          }
110
+        });
76 111
       }
77
-    }
112
+      // top-level entry-point of recursive descent
113
+      rec(container);
78 114
 
79
-    function extractPanelsRoot(items) {
80
-      return isPanelsRoot(items[0]) ? items[0] : null;
81
-    }
115
+      function extractKey(item) {
116
+        return angular.isDefined(item) && _data[item.uid()].groupingKey;
117
+      }
82 118
 
83
-    return _.memoize(function(container) {
84
-      var items = container._getContents();
85
-      var panels = [];
86
-      utils.groupByMetaKey(items, 'panelIndex').forEach(function(items) {
87
-        var panelsRoot = extractPanelsRoot(items);
88
-        if ( panelsRoot ) {
89
-          panelsRoot.getIDs().forEach(function(id) {
90
-            panels.push(Object.create(panelProto).create(panelsRoot, id));
91
-          });
119
+      utils.groupByExtractedKey(items, extractKey).forEach(function(items) {
120
+        var parent, enumerator, obj, context;
121
+        if (items.length > 1 || !items[0].instanceof(Barricade.Container)) {
122
+          parent = _data[items[0].uid()].container;
123
+          // the enumerator function mimicking the behavior of built-in .each()
124
+          // method which aggregate panels do not have
125
+          enumerator = function(callback) {
126
+            items.forEach(function(item) {
127
+              if (_data[item.uid()].container === parent) {
128
+                callback(_data[item.uid()].indexOrKey, item);
129
+              }
130
+            });
131
+          };
92 132
         } else {
93
-          panels.push(Object.create(panelProto).create(items));
133
+          obj = items[0];
134
+          enumerator = obj.each;
135
+          context = _data[obj.uid()];
94 136
         }
137
+        panels.push(Object.create(panelProto).create(enumerator, obj, context));
95 138
       });
96 139
       return utils.condense(panels);
97
-    }, function(container) {
140
+    }, function(container, keyExtractor) {
98 141
       var hash = '';
99
-      container.getKeys().map(function(key) {
100
-        var item = container.get(key);
101
-        if ( isPanelsRoot(item) ) {
102
-          item.getIDs().forEach(function(id) {
103
-            hash += item.getByID(id).uid();
104
-          });
105
-        } else {
106
-          hash += item.uid();
107
-        }
108
-      });
142
+      function rec(container) {
143
+        container.each(function(indexOrKey, item) {
144
+          var groupingKey = keyExtractor(item, container);
145
+          if (angular.isNumber(groupingKey)) {
146
+            hash += item.uid();
147
+          } else if (item.instanceof(Barricade.Container)) {
148
+            rec(item);
149
+          }
150
+        });
151
+      }
152
+      rec(container);
109 153
       return hash;
110 154
     });
111 155
   }
112 156
 
113
-  function extractRows(utils) {
114
-    function getItems(panelOrContainer) {
115
-      if ( panelOrContainer.items ) {
116
-        return panelOrContainer.items;
117
-      } else if ( panelOrContainer.getKeys ) {
118
-        return panelOrContainer.getKeys().map(function(key) {
119
-          return panelOrContainer.get(key);
120
-        });
121
-      } else {
122
-        return panelOrContainer.getIDs().map(function(id) {
123
-          return panelOrContainer.getByID(id);
124
-        });
125
-      }
126
-    }
127
-
128
-    return _.memoize(function(panel) {
129
-      var rowProto = {
130
-          create: function(items) {
131
-            this.id = items[0].uid();
132
-            this.index = items.row;
133
-            this.items = items.slice();
134
-            return this;
135
-          }
136
-        };
137
-
138
-      return utils.groupByMetaKey(getItems(panel), 'row').map(function(items) {
139
-        return Object.create(rowProto).create(items);
157
+  function extractFields() {
158
+    return _.memoize(function(container) {
159
+      var fields = {};
160
+      container.each(function(key, item) {
161
+        fields[key] = item;
140 162
       });
163
+      return fields;
141 164
     }, function(panel) {
142 165
       var hash = '';
143
-      getItems(panel).forEach(function(item) {
166
+      panel.each(function(key, item) {
144 167
         hash += item.uid();
145 168
       });
146 169
       return hash;
147 170
     });
148 171
   }
149 172
 
150
-  function extractItems(utils) {
151
-    return _.memoize(function(row) {
152
-      return row.items.sort(function(item1, item2) {
153
-        return utils.getMeta(item1, 'index') - utils.getMeta(item2, 'index');
154
-      });
155
-    }, function(row) {
173
+  function chunks() {
174
+    return _.memoize(function(fields, itemsPerChunk) {
175
+      var chunks = [];
176
+      var keys = Object.keys(fields);
177
+      var i, j, chunk;
178
+      itemsPerChunk = +itemsPerChunk;
179
+      if (!angular.isNumber(itemsPerChunk) || itemsPerChunk < 1) {
180
+        return chunks;
181
+      }
182
+      for (i = 0; i < keys.length; i++) {
183
+        chunk = {};
184
+        for (j = 0; j < itemsPerChunk; j++) {
185
+          chunk[keys[i]] = fields[keys[i]];
186
+        }
187
+        chunks.push(chunk);
188
+      }
189
+      return chunks;
190
+    }, function(fields) {
156 191
       var hash = '';
157
-      row.items.forEach(function(item) {
158
-        hash += item.uid();
159
-      });
192
+      var key;
193
+      for (key in fields) {
194
+        if (fields.hasOwnProperty(key)) {
195
+          hash += fields[key].uid();
196
+        }
197
+      }
160 198
       return hash;
161 199
     });
162 200
   }
201
+
163 202
 })();

+ 1
- 1
merlin/static/merlin/js/merlin.init.js View File

@@ -19,7 +19,7 @@
19 19
   function fieldTemplates() {
20 20
     return [
21 21
       'dictionary', 'frozendict', 'list',
22
-      'string', 'text', 'group', 'number', 'choices'
22
+      'string', 'text', 'number', 'choices'
23 23
     ];
24 24
   }
25 25
 

+ 15
- 3
merlin/static/merlin/js/merlin.utils.js View File

@@ -23,16 +23,16 @@
23 23
       return 'id-' + idCounter;
24 24
     }
25 25
 
26
-    function groupByMetaKey(sequence, metaKey, insertAtBeginning) {
26
+    function groupByExtractedKey(sequence, keyExtractor, insertAtBeginning) {
27 27
       var newSequence = [];
28 28
       var defaultBucket = [];
29 29
       var index;
30 30
       sequence.forEach(function(item) {
31
-        index = getMeta(item, metaKey);
31
+        index = keyExtractor(item);
32 32
         if ( angular.isDefined(index) ) {
33 33
           if ( !newSequence[index] ) {
34 34
             newSequence[index] = [];
35
-            newSequence[index][metaKey] = index;
35
+            newSequence[index][keyExtractor()] = index;
36 36
           }
37 37
           newSequence[index].push(item);
38 38
         } else {
@@ -51,6 +51,17 @@
51 51
       return newSequence;
52 52
     }
53 53
 
54
+    function groupByMetaKey(sequence, metaKey, insertAtBeginning) {
55
+      function keyExtractor(item) {
56
+        if (angular.isDefined(item)) {
57
+          return getMeta(item, metaKey);
58
+        } else {
59
+          return metaKey;
60
+        }
61
+      }
62
+      return groupByExtractedKey(sequence, keyExtractor, insertAtBeginning);
63
+    }
64
+
54 65
     function getMeta(item, key) {
55 66
       if ( item ) {
56 67
         var meta = item._schema['@meta'];
@@ -103,6 +114,7 @@
103 114
       getMeta: getMeta,
104 115
       getNewId: getNewId,
105 116
       groupByMetaKey: groupByMetaKey,
117
+      groupByExtractedKey: groupByExtractedKey,
106 118
       makeTitle: makeTitle,
107 119
       getNextIDSuffix: getNextIDSuffix,
108 120
       enhanceItemWithID: enhanceItemWithID,

+ 14
- 85
merlin/static/merlin/scss/merlin.scss View File

@@ -1,53 +1,8 @@
1
-@import "/bootstrap/scss/bootstrap";
2
-
3
-.two-panels {
4
-  @include make-row();
5
-  .left-panel {
6
-    @include make-xs-column(6);
7
-  }
8
-  .right-panel {
9
-    @include make-xs-column(6);
10
-  }
11
-  .full-width {
12
-    @include make-xs-column(12);
13
-  }
14
-}
15
-
16
-.two-columns {
17
-  @include make-row();
18
-  .left-column {
19
-    @include make-xs-column(6);
20
-  }
21
-  .right-column {
22
-    @include make-xs-column(6);
23
-  }
24
-}
25
-
26
-.three-columns {
27
-  @include make-row();
28
-  .left-column {
29
-    @include make-xs-column(5);
30
-  }
31
-  .right-column {
32
-    @include make-xs-column(5);
33
-  }
34
-  .both-columns {
35
-    @include make-xs-column(10);
36
-  }
37
-  .button-column {
38
-    @include make-xs-column(2);
39
-  }
40
-}
41
-
42 1
 .panel-default.merlin-panel {
43 2
   .panel-heading {
44 3
     color: inherit;
45 4
     background-color: inherit;
46 5
     border: none;
47
-    padding-left: 20px;
48
-  }
49
-  .panel-body {
50
-    padding-left: 20px;
51 6
   }
52 7
   textarea {
53 8
     resize: vertical;
@@ -64,20 +19,16 @@ editable {
64 19
 }
65 20
 
66 21
 .section {
67
-  .form-group {
68
-    padding-left: 15px;
69
-  }
70
-  .section {
71
-    margin-left: 15px;
72
-  }
73
-  a {
74
-    padding-left: 5px;
75
-    text-decoration: none;
76
-    color: black;
77
-  }
78 22
   h5 {
79 23
     font-weight: bold;
80 24
   }
25
+
26
+  .section-heading {
27
+    a {
28
+      text-decoration: none;
29
+      color: black;
30
+    }
31
+  }
81 32
 }
82 33
 
83 34
 .fa-minus-circle {
@@ -93,28 +44,8 @@ editable {
93 44
   }
94 45
 }
95 46
 
96
-.popover-content > button {
97
-  margin: 5px;
98
-  float: right;
99
-}
100
-
101
-.popover.right {
102
-  width: 200px;
103
-}
104
-
105
-.dictionary .add-btn {
106
-  margin-top: 26px;
107
-}
108
-
109 47
 .list .add-btn {
110
-  margin-top: 2px;
111
-  &.varlist-1st-row {
112
-    margin-top: 26px;
113
-  }
114
-}
115
-
116
-.right-column .form-group {
117
-  padding-left: 0;
48
+  margin-bottom: 15px;
118 49
 }
119 50
 
120 51
 .well .panel-body pre {
@@ -124,12 +55,10 @@ editable {
124 55
 }
125 56
 
126 57
 i.fa-times-circle {
127
-  padding-right: 10px;
128
-  .section .section & {
129
-    font-weight: bold;
130
-    margin-top: 10px;
131
-    margin-bottom: 0;
132
-    font-size: 15px;
133
-    color: inherit;
58
+  font-size: 15px;
59
+  color: inherit;
60
+
61
+  .section .section .section-heading & {
62
+    margin-top: 7px;
134 63
   }
135
-}
64
+};

+ 6
- 6
merlin/static/merlin/templates/collapsible-group.html View File

@@ -1,18 +1,18 @@
1 1
 <div class="section">
2
-  <div class="section-heading three-columns">
3
-    <div class="both-columns">
2
+  <div class="section-heading row">
3
+    <div class="col-xs-10">
4 4
       <h5><a ng-click="isCollapsed = !isCollapsed" class="collapse-entries" href="">
5 5
         <i class="fa" ng-class="isCollapsed ? 'fa-plus-square-o' : 'fa-minus-square-o'"></i></a>
6
-        <editable ng-if="removable" ng-model="group.title"
6
+        <editable ng-if="editable" ng-model="title"
7 7
                   ng-model-options="{getterSetter: true}"></editable>
8
-        <span ng-if="!removable">{$ group.title() $}</span>
8
+        <span ng-if="!editable">{$ ::title $}</span>
9 9
       </h5>
10 10
     </div>
11
-    <div ng-if="additive" class="add-btn button-column add-entry">
11
+    <div ng-if="additive" class="add-btn col-xs add-entry">
12 12
       <button class="btn btn-default btn-sm pull-right" ng-click="onAdd()">
13 13
         <i class="fa fa-plus"></i></button>
14 14
     </div>
15
-    <div ng-if="removable" class="add-btn button-column remove-entry">
15
+    <div ng-if="removable" class="add-btn col-xs remove-entry">
16 16
       <a href="" ng-click="onRemove()">
17 17
         <i class="fa fa-times-circle pull-right"></i></a>
18 18
     </div>

+ 4
- 2
merlin/static/merlin/templates/collapsible-panel.html View File

@@ -1,9 +1,11 @@
1 1
 <div class="panel panel-default merlin-panel">
2
-  <div class="panel-heading" ng-show="panel.title()">
2
+  <div class="panel-heading" ng-show="panel.title">
3 3
     <h4 class="panel-title">
4 4
       <a ng-click="isCollapsed = !isCollapsed" href="">
5 5
         <i class="fa fa-lg" ng-class="isCollapsed ? 'fa-caret-right' : 'fa-caret-down'"></i></a>
6
-      <editable ng-model="panel.title" ng-model-options="{getterSetter: true}"></editable>
6
+      <editable ng-if="editable" ng-model="panel.title"
7
+                ng-model-options="{getterSetter: true}"></editable>
8
+      <span ng-if="!editable">{$ ::panel.title $}</span>
7 9
       <a href="" ng-show="panel.removable" ng-click="panel.remove()">
8 10
         <i class="fa fa-times-circle pull-right"></i></a>
9 11
     </h4>

+ 13
- 16
merlin/static/merlin/templates/fields/choices.html View File

@@ -1,16 +1,13 @@
1
-<div class="form-group">
2
-  <label for="elem-{$ $id $}">{$ value.title() $}</label>
3
-  <select ng-if="value.isDropDown()"
4
-          id="elem-{$ $id $}" class="form-control"
5
-          ng-model="value.value" ng-model-options="{getterSetter: true}">
6
-    <option ng-repeat="option in value.getValues()"
7
-            value="{$ option $}"
8
-            ng-selected="value.get() == option">{$ value.getLabel(option) $}</option>
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>
16
-</div>
1
+<select ng-if="value.isDropDown()"
2
+        id="{$ value.uid() $}" class="form-control"
3
+        ng-model="value.value" ng-model-options="{getterSetter: true}">
4
+  <option ng-repeat="option in value.getValues()"
5
+          value="{$ option $}"
6
+          ng-selected="value.get() == option">{$ value.getLabel(option) $}</option>
7
+</select>
8
+<input ng-if="!value.isDropDown()"
9
+       type="text" class="form-control" id="{$ value.uid() $}"
10
+       ng-model="value.value" ng-model-options="{ getterSetter: true }"
11
+       validatable-with="value" typeahead-editable="true"
12
+       typeahead="option for option in value.getValues() | filter:$viewValue">
13
+<div ng-show="error" class="alert alert-danger">{$ error $}</div>

+ 28
- 13
merlin/static/merlin/templates/fields/dictionary.html View File

@@ -1,19 +1,34 @@
1
-<collapsible-group content="value" on-add="value.add()">
2
-  <div class="three-columns" ng-repeat="subvalue in value.getValues() track by subvalue.keyValue()">
3
-    <div class="left-column">
4
-      <div class="form-group">
5
-        <label for="elem-{$ $id $}.{$ subvalue.uid() $}">
6
-          <editable ng-model="subvalue.keyValue" ng-model-options="{getterSetter: true}"></editable>
1
+<div class="row bottom-xs dictionary">
2
+  <div ng-class="value.inline ? 'col-xs-10' : 'col-xs-12'">
3
+    <div ng-repeat="(key, field) in value | extractFields track by field.uid()">
4
+      <div ng-if="field.isAtomic()" class="form-group">
5
+        <label for="{$ field.uid() $}">
6
+          <editable ng-model="field.keyValue" ng-model-options="{getterSetter: true}"></editable>
7 7
         </label>
8 8
         <div class="input-group">
9
-          <input id="elem-{$ $id $}.{$ subvalue.uid() $}" type="text" class="form-control"
10
-                 ng-model="subvalue.value" ng-model-options="{ getterSetter: true }">
11
-          <span class="input-group-btn">
12
-            <button class="btn btn-default fa fa-minus-circle" type="button"
13
-                ng-click="value.removeItem(subvalue.keyValue())"></button>
14
-          </span>
9
+          <typed-field id="{$ field.uid() $}" value="field" type="{$ field.getType() $}"></typed-field>
10
+        <span class="input-group-btn">
11
+          <button class="btn btn-default" ng-click="value.removeItem(field.keyValue())">
12
+            <i class="fa fa-minus-circle"></i>
13
+          </button>
14
+        </span>
15 15
         </div>
16 16
       </div>
17
+      <div ng-if="!field.isAtomic()">
18
+        <collapsible-group ng-if="!field.inline" content="field"
19
+                           class="col-xs-12"
20
+                           title="field.keyValue"
21
+                           on-remove="value.removeItem(field.keyValue())"
22
+                           additive="{$ field.isAdditive() $}" on-add="field.add()">
23
+          <typed-field value="field" type="{$ field.getType() $}"></typed-field>
24
+        </collapsible-group>
25
+        <typed-field ng-if="field.inline"
26
+                     value="field" type="{$ field.getType() $}"></typed-field>
27
+      </div>
17 28
     </div>
18 29
   </div>
19
-</collapsible-group>
30
+  <div ng-if="value.inline" class="col-xs add-entry" style="margin-bottom: 15px">
31
+    <button class="btn btn-default btn-sm pull-right" ng-click="value.add()">
32
+      <i class="fa fa-plus"></i></button>
33
+  </div>
34
+</div>

+ 21
- 12
merlin/static/merlin/templates/fields/frozendict.html View File

@@ -1,15 +1,24 @@
1
-<collapsible-group content="value">
2
-  <div ng-repeat="row in value | extractRows track by row.id">
3
-    <div ng-class="{'three-columns': row.index !== undefined}">
4
-      <div ng-repeat="item in row | extractItems track by item.uid()"
5
-           ng-class="{'right-column': item.isAtomic() && $odd, 'left-column': item.isAtomic() && $even}">
6
-        <div class="form-group">
7
-          <label for="elem-{$ $id $}.{$ item.uid() $}">{$ item.title() $}</label>
8
-          <input type="text" class="form-control" id="elem-{$ $id $}.{$ item.uid() $}" ng-model="item.value"
9
-                 ng-model-options="{getterSetter: true}">
10
-        </div>
11
-        <div class="clearfix" ng-if="$odd"></div>
1
+<div class="frozendict">
2
+  <div ng-repeat="row in value | extractFields | chunks:2 track by $index">
3
+    <div ng-repeat="(key, field) in row track by field.uid()">
4
+      <div ng-if="field.isAtomic()" class="col-xs-6">
5
+        <labeled label="{$ key $}" for="{$ field.uid() $}">
6
+          <typed-field value="field" type="{$ field.getType() $}"></typed-field>
7
+        </labeled>
8
+      </div>
9
+      <div ng-if="!field.isAtomic()">
10
+        <collapsible-group ng-if="!field.inline" class="col-xs-12"
11
+                           content="field" title="key"
12
+                           additive="{$ field.isAdditive() $}" on-add="field.add()">
13
+          <div ng-class="field.isPlainStructure() ? 'col-xs-6' : 'col-xs-12'">
14
+            <typed-field value="field" type="{$ field.getType() $}"></typed-field>
15
+          </div>
16
+        </collapsible-group>
17
+        <labeled ng-if="field.inline" class="col-xs-6"
18
+                 label="{$ key $}" for="{$ field.uid() $}">
19
+          <typed-field value="field" type="{$ field.getType() $}"></typed-field>
20
+        </labeled>
12 21
       </div>
13 22
     </div>
14 23
   </div>
15
-</collapsible-group>
24
+</div>

+ 0
- 13
merlin/static/merlin/templates/fields/group.html View File

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

+ 18
- 11
merlin/static/merlin/templates/fields/list.html View File

@@ -1,16 +1,23 @@
1
-<collapsible-group content="value" on-add="value.add()">
2
-  <div class="three-columns">
3
-    <div class="left-column">
4
-      <div class="form-group" ng-repeat="subItem in value.getValues() track by $index">
1
+<div class="row bottom-xs list">
2
+  <div ng-class="value.inline ? 'col-xs-10' : 'col-xs-12'">
3
+    <div ng-repeat="(index, field) in value | extractFields track by field.uid()">
4
+      <div ng-if="field.isAtomic()" class="form-group">
5 5
         <div class="input-group">
6
-          <input type="text" class="form-control" ng-model="subItem.value" ng-model-options="{ getterSetter: true }">
7
-          <span class="input-group-btn">
8
-            <button class="btn btn-default" ng-click="value.remove($index)">
9
-              <i class="fa fa-minus-circle"></i>
10
-            </button>
11
-          </span>
6
+          <typed-field value="field" type="{$ field.getType() $}"></typed-field>
7
+        <span class="input-group-btn">
8
+          <button class="btn btn-default" ng-click="value.remove($index)">
9
+            <i class="fa fa-minus-circle"></i>
10
+          </button>
11
+        </span>
12 12
         </div>
13 13
       </div>
14
+      <div ng-if="!field.isAtomic()">
15
+        <typed-field value="field" type="{$ field.getType() $}"></typed-field>
16
+      </div>
14 17
     </div>
15 18
   </div>
16
-</collapsible-group>
19
+  <div ng-if="value.inline" class="col-xs add-btn">
20
+    <button class="btn btn-default btn-sm pull-right" ng-click="value.add()">
21
+      <i class="fa fa-plus"></i></button>
22
+  </div>
23
+</div>

+ 4
- 8
merlin/static/merlin/templates/fields/number.html View File

@@ -1,8 +1,4 @@
1
-<div class="form-group">
2
-  <pre>{$ value $}, {$ value.title() $}</pre>
3
-  <label for="elem-{$ $id $}">{$ value.title() $}</label>
4
-  <input type="number" class="form-control" id="elem-{$ $id $}"
5
-         ng-model="value.value" ng-model-options="{ getterSetter: true }"
6
-         validatable-with="value">
7
-  <div ng-show="error" class="alert alert-danger">{$ error $}</div>
8
-</div>
1
+<input type="number" class="form-control" id="{$ value.uid() $}"
2
+       ng-model="value.value" ng-model-options="{ getterSetter: true }"
3
+       validatable-with="value">
4
+<div ng-show="error" class="alert alert-danger">{$ error $}</div>

+ 4
- 7
merlin/static/merlin/templates/fields/string.html View File

@@ -1,7 +1,4 @@
1
-<div class="form-group">
2
-  <label for="elem-{$ $id $}">{$ value.title() $}</label>
3
-  <input type="text" class="form-control" id="elem-{$ $id $}"
4
-         ng-model="value.value" ng-model-options="{ getterSetter: true }"
5
-         validatable-with="value">
6
-  <div ng-show="error" class="alert alert-danger">{$ error $}</div>
7
-</div>
1
+<input type="text" class="form-control" id="{$ value.uid() $}"
2
+       ng-model="value.value" ng-model-options="{ getterSetter: true }"
3
+       validatable-with="value">
4
+<div ng-show="error" class="alert alert-danger">{$ error $}</div>

+ 4
- 7
merlin/static/merlin/templates/fields/text.html View File

@@ -1,7 +1,4 @@
1
-<div class="form-group">
2
-  <label for="elem-{$ $id $}">{$ value.title() $}</label>
3
-  <textarea class="form-control" id="elem-{$ $id $}"
4
-            ng-model="value.value" ng-model-options="{ getterSetter: true }"
5
-            validatable-with="value"></textarea>
6
-  <div ng-show="error" class="alert alert-danger">{$ error $}</div>
7
-</div>
1
+<textarea class="form-control" id="{$ value.uid() $}"
2
+          ng-model="value.value" ng-model-options="{ getterSetter: true }"
3
+          validatable-with="value"></textarea>
4
+<div ng-show="error" class="alert alert-danger">{$ error $}</div>

+ 4
- 0
merlin/static/merlin/templates/labeled.html View File

@@ -0,0 +1,4 @@
1
+<div class="form-group">
2
+  <label for="{$ for $}">{$ label $}</label>
3
+  <div ng-transclude></div>
4
+</div>

+ 1
- 1
merlin/test/js/merlin.directives.spec.js View File

@@ -66,7 +66,7 @@ describe('merlin directives', function() {
66 66
       return element;
67 67
     }
68 68
 
69
-    it('shows panel heading when and only when its title() is not false', function() {
69
+    it('shows panel heading when and only when its title is defined', function() {
70 70
       var title = 'My Panel',
71 71
         element1, element2;
72 72
 

+ 305
- 649
merlin/test/js/merlin.filters.spec.js
File diff suppressed because it is too large
View File


+ 4
- 30
merlin/test/js/merlin.models.spec.js View File

@@ -47,41 +47,18 @@ describe('merlin models:', function() {
47 47
       return value;
48 48
     }
49 49
 
50
-    function getCacheIDs() {
51
-      return dictObj.getValues().map(function(item) {
52
-        return item.getID();
53
-      });
54
-    }
55
-
56
-    describe('getValues() method', function() {
57
-      it('caching works from the very beginning', function() {
58
-        expect(getCacheIDs()).toEqual(['id1', 'id2']);
59
-      });
60
-
61
-      it('keyValue() getter/setter can be used from the start', function() {
62
-        var value = getValueFromCache('id1');
63
-
64
-        expect(value.keyValue()).toBe('id1');
65
-
66
-        value.keyValue('id3');
67
-        expect(value.keyValue()).toBe('id3');
68
-        expect(dictObj.getByID('id3')).toBeDefined();
69
-      });
70
-    });
71
-
72 50
     describe('add() method', function() {
73 51
       it('adds an empty value with given key', function() {
74 52
         dictObj.add('id3');
75 53
 
76 54
         expect(dictObj.getByID('id3').get()).toBe('');
77
-        expect(getCacheIDs()).toEqual(['id1', 'id2', 'id3']);
78 55
       });
79 56
 
80 57
       it('keyValue() getter/setter can be used for added values', function() {
81 58
         var value;
82 59
 
83 60
         dictObj.add('id3');
84
-        value = getValueFromCache('id3');
61
+        value = dictObj.getByID('id3');
85 62
 
86 63
         expect(value.keyValue()).toBe('id3');
87 64
 
@@ -112,31 +89,28 @@ describe('merlin models:', function() {
112 89
     });
113 90
 
114 91
     describe('empty() method', function() {
115
-      it('removes all entries in model and in cache', function() {
92
+      it('removes all entries in model', function() {
116 93
         dictObj.empty();
117 94
 
118 95
         expect(dictObj.getIDs().length).toBe(0);
119
-        expect(dictObj.getValues().length).toBe(0);
120 96
       })
121 97
     });
122 98
 
123 99
     describe('resetKeys() method', function() {
124
-      it('re-sets dictionary contents to given keys, cache included', function() {
100
+      it('re-sets dictionary contents to given keys', function() {
125 101
         dictObj.resetKeys(['key1', 'key2']);
126 102
 
127 103
         expect(dictObj.getIDs()).toEqual(['key1', 'key2']);
128 104
         expect(dictObj.getByID('key1').get()).toBe('');
129 105
         expect(dictObj.getByID('key2').get()).toBe('');
130
-        expect(getCacheIDs()).toEqual(['key1', 'key2']);
131 106
       })
132 107
     });
133 108
 
134 109
     describe('removeItem() method', function() {
135
-      it('removes dictionary entry by key from model and cache', function() {
110
+      it('removes dictionary entry by key from model', function() {
136 111
         dictObj.removeItem('id1');
137 112
 
138 113
         expect(dictObj.getByID('id1')).toBeUndefined();
139
-        expect(getCacheIDs()).toEqual(['id2']);
140 114
       })
141 115
     });
142 116
 

Loading…
Cancel
Save