button panels done right
[plstackapi.git] / planetstack / core / xoslib / static / js / xoslib / xosHelper.js
1 HTMLView = Marionette.ItemView.extend({
2   render: function() {
3       this.$el.append(this.options.html);
4   },
5 });
6
7 FilteredCompositeView = Marionette.CompositeView.extend( {
8     showCollection: function() {
9       var ChildView;
10       this.collection.each(function(child, index) {
11         if (this.filter && !this.filter(child)) {
12             return;
13         }
14         ChildView = this.getChildView(child);
15         this.addChild(child, ChildView, index);
16       }, this);
17
18     },
19 });
20
21 XOSRouter = Marionette.AppRouter.extend({
22         initialize: function() {\r
23             this.routeStack=[];\r
24         },\r
25 \r
26         onRoute: function(x,y,z) {\r
27              this.routeStack.push(Backbone.history.fragment);\r
28         },\r
29 \r
30         prevPage: function() {\r
31              return this.routeStack.slice(-1)[0];
32         },
33
34         showPreviousURL: function() {
35             prevPage = this.prevPage();
36             console.log("showPreviousURL");
37             console.log(this.routeStack);
38             if (prevPage) {
39                 this.navigate("#"+prevPage, {trigger: false, replace: true} );
40             }
41         },
42     });\r
43
44
45
46 XOSApplication = Marionette.Application.extend({
47     detailBoxId: "#detailBox",
48     errorBoxId: "#errorBox",
49     errorCloseButtonId: "#close-error-box",
50     successBoxId: "#successBox",
51     successCloseButtonId: "#close-success-box",
52     errorTemplate: "#xos-error-template",
53     successTemplate: "#xos-success-template",
54     logMessageCount: 0,
55
56     confirmDialog: function(view, event, callback) {
57         $("#xos-confirm-dialog").dialog({
58            autoOpen: false,
59            modal: true,
60            buttons : {
61                 "Confirm" : function() {
62                   $(this).dialog("close");
63                   if (event) {
64                       view.trigger(event);
65                   }
66                   if (callback) {
67                       callback();
68                   }
69                 },
70                 "Cancel" : function() {
71                   $(this).dialog("close");
72                 }
73               }
74             });
75         $("#xos-confirm-dialog").dialog("open");
76     },
77
78     popupErrorDialog: function(responseText) {
79         try {
80             parsed_error=$.parseJSON(responseText);
81             width=300;
82         }
83         catch(err) {
84             parsed_error=undefined;
85             width=640;    // django stacktraces like wide width
86         }
87         if (parsed_error) {
88             $("#xos-error-dialog").html(templateFromId("#xos-error-response")(parsed_error));
89         } else {
90             $("#xos-error-dialog").html(templateFromId("#xos-error-rawresponse")({responseText: responseText}))
91         }
92
93         $("#xos-error-dialog").dialog({
94             modal: true,
95             width: width,
96             buttons: {
97                 Ok: function() { $(this).dialog("close"); }
98             }
99         });
100     },
101
102     hideLinkedItems: function(result) {
103         var index=0;
104         while (index<4) {
105             this["linkedObjs" + (index+1)].empty();
106             index = index + 1;
107         }
108     },
109
110     createListHandler: function(listViewName, collection_name, regionName, title) {
111         var app=this;
112         return function() {
113             listView = new app[listViewName];
114             app[regionName].show(listView);
115             app.hideLinkedItems();
116             $("#contentTitle").html(templateFromId("#xos-title-list")({"title": title}));
117             $("#detail").show();
118             $("#tabs").hide();
119
120             listButtons = new XOSListButtonView({linkedView: listView});
121             app["rightButtonPanel"].show(listButtons);
122         }
123     },
124
125     createAddHandler: function(detailName, collection_name, regionName, title) {
126         var app=this;
127         return function() {
128             model = new xos[collection_name].model();
129             detailViewClass = app[detailName];
130             detailView = new detailViewClass({model: model, collection:xos[collection_name]});
131             app[regionName].show(detailView);
132             $("#xos-detail-button-box").show();
133             $("#xos-listview-button-box").hide();
134         }
135     },
136
137     createAddChildHandler: function(addChildName, collection_name) {
138         var app=this;
139         return function(parent_modelName, parent_fieldName, parent_id) {
140             app.Router.showPreviousURL();
141             console.log("acs");
142             console.log(parent_modelName);
143             console.log(parent_fieldName);
144             console.log(parent_id);
145             model = new xos[collection_name].model();
146             model.attributes[parent_fieldName] = parent_id;
147             model.readOnlyFields.push(parent_fieldName);
148             console.log(model);
149             detailViewClass = app[addChildName];
150             var detailView = new detailViewClass({model: model, collection:xos[collection_name]});
151             detailView.dialog = $("xos-addchild-dialog");
152             app["addChildDetail"].show(detailView);
153             $("#xos-addchild-dialog").dialog({
154                autoOpen: false,
155                modal: true,
156                width: 640,
157                buttons : {
158                     "Save" : function() {
159                       var addDialog = this;
160                       detailView.synchronous = true;
161                       detailView.afterSave = function() { console.log("afterSave"); $(addDialog).dialog("close"); }
162                       detailView.save();
163
164                       //$(this).dialog("close");
165                     },
166                     "Cancel" : function() {
167                       $(this).dialog("close");
168                     }
169                   }
170                 });
171             $("#xos-addchild-dialog").dialog("open");
172         }
173     },
174
175     createDeleteHandler: function(collection_name) {
176         var app=this;
177         return function(model_id) {
178             console.log("deleteCalled");
179             collection = xos[collection_name];
180             model = collection.get(model_id);
181             assert(model!=undefined, "failed to get model " + model_id + " from collection " + collection_name);
182             app.Router.showPreviousURL();
183             app.deleteDialog(model);
184         }
185     },
186
187     createDetailHandler: function(detailName, collection_name, regionName, title) {
188         var app=this;
189         showModelId = function(model_id) {
190             $("#contentTitle").html(templateFromId("#xos-title-detail")({"title": title}));
191
192             collection = xos[collection_name];
193             model = collection.get(model_id);
194             if (model == undefined) {
195                 app[regionName].show(new HTMLView({html: "failed to load object " + model_id + " from collection " + collection_name}));
196             } else {
197                 detailViewClass = app[detailName];
198                 detailView = new detailViewClass({model: model});
199                 app[regionName].show(detailView);
200                 detailView.showLinkedItems();
201                 //$("#xos-detail-button-box").show();
202                 //$("#xos-listview-button-box").hide();
203
204                 detailButtons = new XOSDetailButtonView({linkedView: detailView});
205                 app["rightButtonPanel"].show(detailButtons);
206             }
207         }
208         return showModelId;
209     },
210
211     /* error handling callbacks */
212
213     hideError: function() {
214         if (this.logWindowId) {
215         } else {
216             $(this.errorBoxId).hide();
217             $(this.successBoxId).hide();
218         }
219     },
220
221     showSuccess: function(result) {
222          result["statusclass"] = "success";
223          if (this.logTableId) {
224              this.appendLogWindow(result);
225          } else {
226              $(this.successBoxId).show();
227              $(this.successBoxId).html(_.template($(this.successTemplate).html())(result));
228              var that=this;
229              $(this.successCloseButtonId).unbind().bind('click', function() {
230                  $(that.successBoxId).hide();
231              });
232          }
233     },
234
235     showError: function(result) {
236          result["statusclass"] = "failure";
237          if (this.logTableId) {
238              this.appendLogWindow(result);
239              this.popupErrorDialog(result.responseText);
240          } else {
241              // this is really old stuff
242              $(this.errorBoxId).show();
243              $(this.errorBoxId).html(_.template($(this.errorTemplate).html())(result));
244              var that=this;
245              $(this.errorCloseButtonId).unbind().bind('click', function() {
246                  $(that.errorBoxId).hide();
247              });
248          }
249     },
250
251     showInformational: function(result) {
252          result["statusclass"] = "inprog";
253          if (this.logTableId) {
254              return this.appendLogWindow(result);
255          } else {
256              return undefined;
257          }
258     },
259
260     appendLogWindow: function(result) {
261         // compute a new logMessageId for this log message
262         logMessageId = "logMessage" + this.logMessageCount;
263         this.logMessageCount = this.logMessageCount + 1;
264         result["logMessageId"] = logMessageId;
265
266         logMessageTemplate=$("#xos-log-template").html();
267         assert(logMessageTemplate != undefined, "logMessageTemplate is undefined");
268         newRow = _.template(logMessageTemplate, result);
269         assert(newRow != undefined, "newRow is undefined");
270
271         if (result["infoMsgId"] != undefined) {
272             // We were passed the logMessageId of an informational message,
273             // and the caller wants us to replace that message with our own.
274             // i.e. replace an informational message with a success or an error.
275             $("#"+result["infoMsgId"]).replaceWith(newRow);
276         } else {
277             // Create a brand new log message rather than replacing one.
278             logTableBody = $(this.logTableId + " tbody");
279             logTableBody.prepend(newRow);
280         }
281
282         if (this.statusMsgId) {
283             $(this.statusMsgId).html( templateFromId("#xos-status-template")(result) );
284         }
285
286         limitTableRows(this.logTableId, 5);
287
288         return logMessageId;
289     },
290
291     saveError: function(model, result, xhr, infoMsgId) {
292         console.log("saveError");
293         result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
294         result["infoMsgId"] = infoMsgId;
295         this.showError(result);
296     },
297
298     saveSuccess: function(model, result, xhr, infoMsgId, addToCollection) {
299         console.log("saveSuccess");
300         if (model.addToCollection) {
301             console.log("addToCollection");
302             console.log(model.addToCollection);
303             model.addToCollection.add(model);
304             model.addToCollection.sort();
305             model.addToCollection = undefined;
306         }
307         result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
308         result["what"] = "save " + model.modelName + " " + model.attributes.humanReadableName;
309         result["infoMsgId"] = infoMsgId;
310         this.showSuccess(result);
311     },
312
313     destroyError: function(model, result, xhr, infoMsgId) {
314         result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
315         result["infoMsgId"] = infoMsgId;
316         this.showError(result);
317     },
318
319     destroySuccess: function(model, result, xhr, infoMsgId) {
320         result = {status: xhr.xhr.status, statusText: xhr.xhr.statusText};
321         result["what"] = "destroy " + model.modelName + " " + model.attributes.humanReadableName;
322         result["infoMsgId"] = infoMsgId;
323         this.showSuccess(result);
324     },
325
326     /* end error handling callbacks */
327
328     destroyModel: function(model) {
329          //console.log("destroyModel"); console.log(model);
330          this.hideError();
331          var infoMsgId = this.showInformational( {what: "destroy " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
332          var that = this;
333          model.destroy({error: function(model, result, xhr) { that.destroyError(model,result,xhr,infoMsgId);},
334                         success: function(model, result, xhr) { that.destroySuccess(model,result,xhr,infoMsgId);}});
335     },
336
337     deleteDialog: function(model, afterDelete) {
338         var that=this;
339         assert(model!=undefined, "deleteDialog's model is undefined");
340         //console.log("deleteDialog"); console.log(model);
341         this.confirmDialog(null, null, function() {
342             //console.log("deleteConfirm"); console.log(model);
343             modelName = model.modelName;
344             that.destroyModel(model);
345             if (afterDelete=="list") {
346                 that.navigate("list", modelName);
347             }
348         });
349     },
350 });
351
352 XOSButtonView = Marionette.ItemView.extend({
353             events: {"click button.btn-xos-save-continue": "submitContinueClicked",
354                      "click button.btn-xos-save-leave": "submitLeaveClicked",
355                      "click button.btn-xos-save-another": "submitAddAnotherClicked",
356                      "click button.btn-xos-delete": "deleteClicked",
357                      "click button.btn-xos-add": "addClicked",
358                      "click button.btn-xos-refresh": "refreshClicked",
359                      },
360
361             submitLeaveClicked: function(e) {
362                      this.options.linkedView.submitLeaveClicked.call(this.options.linkedView, e);
363                      },
364
365             submitContinueClicked: function(e) {
366                      this.options.linkedView.submitContinueClicked.call(this.options.linkedView, e);
367                      },
368
369             submitAddAnotherClicked: function(e) {
370                      this.options.linkedView.submitAddAnotherClicked.call(this.options.linkedView, e);
371                      },
372
373             submitDeleteClicked: function(e) {
374                      this.options.linkedView.submitDeleteClicked.call(this.options.linkedView, e);
375                      },
376
377             addClicked: function(e) {
378                      this.options.linkedView.addClicked.call(this.options.linkedView, e);
379                      },
380
381             refreshClicked: function(e) {
382                      this.options.linkedView.refreshClicked.call(this.options.linkedView, e);
383                      },
384             });
385
386 XOSDetailButtonView = XOSButtonView.extend({ template: "#xos-savebuttons-template" });
387 XOSListButtonView = XOSButtonView.extend({ template: "#xos-listbuttons-template" });
388
389 /* XOSDetailView
390       extend with:
391          app - MarionetteApplication
392          template - template (See XOSHelper.html)
393 */
394
395 XOSDetailView = Marionette.ItemView.extend({
396             tagName: "div",
397
398             events: {"click button.btn-xos-save-continue": "submitContinueClicked",
399                      "click button.btn-xos-save-leave": "submitLeaveClicked",
400                      "click button.btn-xos-save-another": "submitAddAnotherClicked",
401                      "click button.btn-xos-delete": "deleteClicked",
402                      "change input": "inputChanged"},
403
404             /* inputChanged is watching the onChange events of the input controls. We
405                do this to track when this view is 'dirty', so we can throw up a warning
406                if the user tries to change his slices without saving first.
407             */
408
409             initialize: function() {
410                 this.on("saveSuccess", this.onAfterSave);
411                 this.synchronous = false;
412             },
413
414             afterSave: function(e) {
415             },
416
417             onAfterSave: function(e) {
418                 this.afterSave(e);
419             },
420
421             inputChanged: function(e) {
422                 this.dirty = true;
423             },
424
425             submitContinueClicked: function(e) {
426                 console.log("saveContinue");
427                 e.preventDefault();
428                 this.afterSave = function() { };
429                 this.save();
430             },
431
432             submitLeaveClicked: function(e) {
433                 console.log("saveLeave");
434                 e.preventDefault();
435                 var that=this;
436                 this.afterSave = function() {
437                     that.app.navigate("list", that.model.modelName);
438                 }
439                 this.save();
440             },
441
442             submitAddAnotherClicked: function(e) {
443                 console.log("saveAnother");
444                 console.log(this);
445                 e.preventDefault();
446                 var that=this;
447                 this.afterSave = function() {
448                     that.app.navigate("add", that.model.modelName);
449                 }
450                 this.save();
451             },
452
453             save: function() {
454                 this.app.hideError();
455                 var data = Backbone.Syphon.serialize(this);
456                 var that = this;
457                 var isNew = !this.model.id;
458
459                 this.$el.find(".help-inline").remove();
460
461                 /* although model.validate() is called automatically by
462                    model.save, we call it ourselves, so we can throw up our
463                    validation error before creating the infoMsg in the log
464                 */
465                 errors =  this.model.xosValidate(data);
466                 if (errors) {
467                     this.onFormDataInvalid(errors);
468                     return;
469                 }
470
471                 if (isNew) {
472                     this.model.attributes.humanReadableName = "new " + model.modelName;
473                     this.model.addToCollection = this.collection;
474                 } else {
475                     this.model.addToCollection = undefined;
476                 }
477
478                 var infoMsgId = this.app.showInformational( {what: "save " + model.modelName + " " + model.attributes.humanReadableName, status: "", statusText: "in progress..."} );
479
480                 this.model.save(data, {error: function(model, result, xhr) { that.app.saveError(model,result,xhr,infoMsgId);},
481                                        success: function(model, result, xhr) { that.app.saveSuccess(model,result,xhr,infoMsgId);
482                                                                                if (that.synchronous) {
483                                                                                    that.trigger("saveSuccess");
484                                                                                }
485                                                                              }});
486                 this.dirty = false;
487
488                 if (!this.synchronous) {
489                     this.afterSave();
490                 }
491             },
492
493             deleteClicked: function(e) {
494                 e.preventDefault();
495                 this.app.deleteDialog(this.model, "list");
496             },
497
498             tabClick: function(tabId, regionName) {
499                     region = this.app[regionName];
500                     if (this.currentTabRegion != undefined) {
501                         this.currentTabRegion.$el.hide();
502                     }
503                     if (this.currentTabId != undefined) {
504                         $(this.currentTabId).removeClass('active');
505                     }
506                     this.currentTabRegion = region;
507                     this.currentTabRegion.$el.show();
508
509                     this.currentTabId = tabId;
510                     $(tabId).addClass('active');
511             },
512
513             showTabs: function(tabs) {
514                 template = templateFromId("#xos-tabs-template", {tabs: tabs});
515                 $("#tabs").html(template(tabs));
516                 var that = this;
517
518                 _.each(tabs, function(tab) {
519                     var regionName = tab["region"];
520                     var tabId = '#xos-nav-'+regionName;
521                     $(tabId).bind('click', function() { that.tabClick(tabId, regionName); });
522                 });
523
524                 $("#tabs").show();
525             },
526
527             showLinkedItems: function() {
528                     tabs=[];
529
530                     tabs.push({name: "details", region: "detail"});
531
532                     makeFilter = function(relatedField, relatedId) {
533                         return function(model) { return model.attributes[relatedField] == relatedId; }
534                     };
535
536                     var index=0;
537                     for (relatedName in this.model.collection.relatedCollections) {
538                         var relatedField = this.model.collection.relatedCollections[relatedName];
539                         var relatedId = this.model.id;
540                         regionName = "linkedObjs" + (index+1);
541
542                         relatedListViewClassName = relatedName + "ListView";
543                         assert(this.app[relatedListViewClassName] != undefined, relatedListViewClassName + " not found");
544                         relatedListViewClass = this.app[relatedListViewClassName].extend({collection: xos[relatedName],
545                                                                                           filter: makeFilter(relatedField, relatedId),
546                                                                                           parentModel: this.model});
547                         this.app[regionName].show(new relatedListViewClass());
548                         if (this.app.hideTabsByDefault) {
549                             this.app[regionName].$el.hide();
550                         }
551                         tabs.push({name: relatedName, region: regionName});
552                         index = index + 1;
553                     }
554
555                     while (index<4) {
556                         this.app["linkedObjs" + (index+1)].empty();
557                         index = index + 1;
558                     }
559
560                     this.showTabs(tabs);
561                     this.tabClick('#xos-nav-detail', 'detail');
562               },
563
564             onFormDataInvalid: function(errors) {
565                 var self=this;
566                 var markErrors = function(value, key) {
567                     console.log("name='" + key + "'");
568                     var $inputElement = self.$el.find("[name='" + key + "']");
569                     var $inputContainer = $inputElement.parent();
570                     //$inputContainer.find(".help-inline").remove();
571                     var $errorEl = $("<span>", {class: "help-inline error", text: value});
572                     $inputContainer.append($errorEl).addClass("error");
573                 }
574                 _.each(errors, markErrors);
575             },
576
577              templateHelpers: function() { return { modelName: this.model.modelName,
578                                                     collectionName: this.model.collectionName,
579                                                     addFields: this.model.addFields,
580                                                     listFields: this.model.listFields,
581                                                     detailFields: this.model.detailFields,
582                                                     foreignFields: this.model.foreignFields,
583                                                     detailLinkFields: this.model.detailLinkFields,
584                                                     inputType: this.model.inputType,
585                                                     model: this.model,
586                                          }},
587
588 });
589
590 /* XOSItemView
591       This is for items that will be displayed as table rows.
592       extend with:
593          app - MarionetteApplication
594          template - template (See XOSHelper.html)
595 */
596
597 XOSItemView = Marionette.ItemView.extend({
598              tagName: 'tr',
599              className: 'test-tablerow',
600
601              templateHelpers: function() { return { modelName: this.model.modelName,
602                                                     collectionName: this.model.collectionName,
603                                                     listFields: this.model.listFields,
604                                                     addFields: this.model.addFields,
605                                                     detailFields: this.model.detailFields,
606                                                     foreignFields: this.model.foreignFields,
607                                                     detailLinkFields: this.model.detailLinkFields,
608                                                     inputType: this.model.inputType,
609                                                     model: this.model,
610                                          }},
611 });
612
613 /* XOSListView:
614       extend with:
615          app - MarionetteApplication
616          childView - class of ItemView, probably an XOSItemView
617          template - template (see xosHelper.html)
618          collection - collection that holds these objects
619          title - title to display in template
620 */
621
622 XOSListView = FilteredCompositeView.extend({
623              childViewContainer: 'tbody',
624              parentModel: null,
625
626              events: {"click button.btn-xos-add": "addClicked",
627                       "click button.btn-xos-refresh": "refreshClicked",
628                      },
629
630              _fetchStateChange: function() {
631                  if (this.collection.fetching) {
632                     $("#xos-list-title-spinner").show();
633                  } else {
634                     $("#xos-list-title-spinner").hide();
635                  }
636              },
637
638              addClicked: function(e) {
639                 e.preventDefault();
640                 this.app.Router.navigate("add" + firstCharUpper(this.collection.modelName), {trigger: true});
641              },
642
643              refreshClicked: function(e) {
644                  e.preventDefault();
645                  this.collection.refresh(refreshRelated=true);
646              },
647
648              initialize: function() {
649                  this.listenTo(this.collection, 'change', this._renderChildren)
650                  this.listenTo(this.collection, 'sort', function() { console.log("sort"); })
651                  this.listenTo(this.collection, 'add', function() { console.log("add"); })
652                  this.listenTo(this.collection, 'fetchStateChange', this._fetchStateChange);
653
654                  // Because many of the templates use idToName(), we need to
655                  // listen to the collections that hold the names for the ids
656                  // that we want to display.
657                  for (i in this.collection.foreignCollections) {
658                      foreignName = this.collection.foreignCollections[i];
659                      if (xos[foreignName] == undefined) {
660                          console.log("Failed to find xos class " + foreignName);
661                      }
662                      this.listenTo(xos[foreignName], 'change', this._renderChildren);
663                      this.listenTo(xos[foreignName], 'sort', this._renderChildren);
664                  }
665              },
666
667              getAddChildHash: function() {
668                 if (this.parentModel) {
669                     parentFieldName = this.parentModel.relatedCollections[this.collection.collectionName];
670                     parentFieldName = parentFieldName || "unknown";
671
672                     /*parentFieldName = "unknown";
673
674                     for (fieldName in this.collection.foreignFields) {
675                         cname = this.collection.foreignFields[fieldName];
676                         if (cname = this.collection.collectionName) {
677                             parentFieldName = fieldName;
678                         }
679                     }*/
680                     return "#addChild" + firstCharUpper(this.collection.modelName) + "/" + this.parentModel.modelName + "/" + parentFieldName + "/" + this.parentModel.id; // modelName, fieldName, id
681                 } else {
682                     return null;
683                 }
684              },
685
686              templateHelpers: function() {
687                 return { title: this.title,
688                          addChildHash: this.getAddChildHash(),
689                          foreignFields: this.collection.foreignFields,
690                          listFields: this.collection.listFields,
691                          detailLinkFields: this.collection.detailLinkFields, };
692              },
693 });
694
695 idToName = function(id, collectionName, fieldName) {
696     return xos.idToName(id, collectionName, fieldName);
697 };
698
699 /* Constructs lists of <option> html blocks for items in a collection.
700
701    selectedId = the id of an object that should be selected, if any
702    collectionName = name of collection
703    fieldName = name of field within models of collection that will be displayed
704 */
705
706 idToOptions = function(selectedId, collectionName, fieldName) {
707     result=""
708     for (index in xos[collectionName].models) {
709         linkedObject = xos[collectionName].models[index];
710         linkedId = linkedObject["id"];
711         linkedName = linkedObject.attributes[fieldName];
712         if (linkedId == selectedId) {
713             selected = " selected";
714         } else {
715             selected = "";
716         }
717         result = result + '<option value="' + linkedId + '"' + selected + '>' + linkedName + '</option>';
718     }
719     return result;
720 };
721
722 /* Constructs an html <select> and the <option>s to go with it.
723
724    variable = variable name to return to form
725    selectedId = the id of an object that should be selected, if any
726    collectionName = name of collection
727    fieldName = name of field within models of collection that will be displayed
728 */
729
730 idToSelect = function(variable, selectedId, collectionName, fieldName, readOnly) {
731     if (readOnly) {
732         readOnly = " readonly";
733     } else {
734         readOnly = "";
735     }
736     result = '<select name="' + variable + '"' + readOnly + '>' +
737              idToOptions(selectedId, collectionName, fieldName) +
738              '</select>';
739     console.log(result);
740     return result;
741 }
742