"}});return{View:e}}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(2),i(1))},function(t,e,i){var n,s;(function(i,o,a){n=[],s=function(){var t=i.Model.extend({defaults:{title:"",icon_class:"",on_click:null,menu_options:null,is_menu_button:!0,id:null,href:null,target:null,enabled:!0,visible:!0,tooltip_config:{}}}),e=i.View.extend({initialize:function(){this.model.attributes.tooltip_config={placement:"bottom"},this.model.bind("change",this.render,this)},render:function(){this.$el.tooltip("hide");var t=this.template(this.model.toJSON());return t.tooltip(this.model.get("tooltip_config")),this.$el.replaceWith(t),this.setElement(t),this},events:{click:"click"},click:function(t){return o.isFunction(this.model.get("on_click"))?(this.model.get("on_click")(t),!1):!0},template:function(t){var e='title="'+t.title+'" class="icon-button';return t.is_menu_button&&(e+=" menu-button"),e+=" "+t.icon_class,t.enabled||(e+="_disabled"),e+='"',t.id&&(e+=' id="'+t.id+'"'),e+=' href="'+t.href+'"',t.target&&(e+=' target="'+t.target+'"'),t.visible||(e+=' style="display: none;"'),e=t.enabled?"":"",a(e)}}),n=i.Collection.extend({model:t}),s=i.View.extend({tagName:"div",initialize:function(){this.render()},render:function(){var t=this;return this.collection.each(function(e){var i=a("").attr("href","javascript:void(0)").attr("title",e.attributes.title).addClass("icon-button menu-button").addClass(e.attributes.icon_class).appendTo(t.$el).click(e.attributes.on_click);e.attributes.tooltip_config&&i.tooltip(e.attributes.tooltip_config);var n=e.get("options");n&&make_popupmenu(i,n)}),this}}),r=function(e,i){i||(i={});var a=new n(o.map(e,function(e){return new t(o.extend(e,i))}));return new s({collection:a})};return{IconButton:t,IconButtonView:e,IconButtonCollection:n,IconButtonMenuView:s,create_icon_buttons_menu:r}}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(2),i(3),i(1))},function(t,e,i){var n,s,o;!function(a){s=[i(1)],n=a,o="function"==typeof n?n.apply(e,s):n,!(void 0!==o&&(t.exports=o))}(function(t){"use_strict";var e=t;e.fn.make_text_editable=function(t){var i="num_cols"in t?t.num_cols:30,n="num_rows"in t?t.num_rows:4,s="use_textarea"in t?t.use_textarea:!1,o="on_finish"in t?t.on_finish:null,a="help_text"in t?t.help_text:null,r=e(this);return r.addClass("editable-text").click(function(a){if(!(e(this).children(":input").length>0)){r.removeClass("editable-text");var l,c,d=function(t){r.find(":input").remove(),""!==t?r.text(t):r.html(" "),r.addClass("editable-text"),o&&o(t)},h="cur_text"in t?t.cur_text:r.text();s?(l=e("").attr({rows:n,cols:i}).text(e.trim(h)).keyup(function(t){27===t.keyCode&&d(h)}),c=e("").text("Done").click(function(){return d(l.val()),!1})):l=e("").attr({value:e.trim(h),size:i}).blur(function(){d(h)}).keyup(function(t){27===t.keyCode?e(this).trigger("blur"):13===t.keyCode&&d(e(this).val()),t.stopPropagation()}),r.text(""),r.append(l),c&&r.append(c),l.focus(),l.select(),a.stopPropagation()}}),a&&r.attr("title",a).tooltip(),r}})},function(t,e,i){var n;(function(s,o){n=function(){function t(t){t+="";for(var e=/(\d+)(\d{3})/;e.test(t);)t=t.replace(e,"$1,$2");return t}var e=function(t){return"promise"in t},i=s.Model.extend({defaults:{ajax_settings:{},interval:1e3,success_fn:function(t){return!0}},go:function(){var t=o.Deferred(),e=this,i=e.get("ajax_settings"),n=e.get("success_fn"),s=e.get("interval"),a=function(){o.ajax(i).success(function(e){n(e)?t.resolve(e):setTimeout(a,s)})};return a(),t}}),n=function(t){t||(t="#ffffff"),"string"==typeof t&&(t=[t]);for(var e=0;e>16,s=(65280&i)>>8,o=255&i,d=p(n,s,o),f=!0,e=0;e>16,l=(65280&a)>>8,c=255&a,h=p(r,l,c),u=g(n,s,o,r,l,c),Math.abs(d-h)<40||200>u){f=!1;break}m++}while(!f&&10>=m);return"#"+(16777216+i).toString(16).substr(1,6)};return{commatize:t,is_deferred:e,ServerStateDeferred:i,get_random_color:n}}.call(e,i,e,t),!(void 0!==n&&(t.exports=n))}).call(e,i(2),i(1))},function(t,e,i){var n,s;(function(o){n=[i(28),i(6),i(5)],s=function(t,e,i){t=t||window.BibtexParser;var n="citation",s=o.Model.extend(e.LoggableMixin).extend({_logNamespace:n,initialize:function(){var e=this.get("content"),i=t(e).entries[0];this.entry=i,this._fields={};var n=i.Fields;for(var s in n){var o=n[s],a=s.toLowerCase();this._fields[a]=o}},entryType:function(){return this.entry.EntryType},fields:function(){return this._fields}}),a=o.Collection.extend(e.LoggableMixin).extend({_logNamespace:n,urlRoot:Galaxy.root+"api",partial:!0,model:s}),r=a.extend({url:function(){return this.urlRoot+"/histories/"+this.history_id+"/citations"}}),l=a.extend({url:function(){return this.urlRoot+"/tools/"+this.tool_id+"/citations"},partial:!1});return{Citation:s,HistoryCitationCollection:r,ToolCitationCollection:l}}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(2))},function(t,e,i){var n,s;n=[i(4),i(7),i(44),i(20)],s=function(t,e,i,n){var s=i.extend({initialize:function(n){var s=this;i.prototype.initialize.call(this,t.merge({customize:function(t){t.buttons={execute:execute_btn=new e.Button({icon:"fa-check",tooltip:"Execute: "+t.name+" ("+t.version+")",title:"Execute",cls:"ui-button btn btn-primary",floating:"clear",onclick:function(){execute_btn.wait(),s.portlet.disable(),s.submit(t,function(){execute_btn.unwait(),s.portlet.enable()})}})},t.job_id&&t.job_remap&&(t.inputs.rerun_remap_job_id={label:"Resume dependencies from this job",name:"rerun_remap_job_id",type:"select",display:"radio",ignore:"__ignore__",value:"__ignore__",options:[["Yes",t.job_id],["No","__ignore__"]],help:"The previous run of this tool failed and other tools were waiting for it to finish successfully. Use this option to resume those tools using the new output(s) of this tool run."})}},n))},submit:function(e,i){var s=this,o={tool_id:e.id,tool_version:e.version,inputs:this.data.create()};return this.trigger("reset"),s.validate(o)?(Galaxy.emit.debug("tool-form::submit()","Validation complete.",o),void t.request({type:"POST",url:Galaxy.root+"api/tools",data:o,success:function(t){i&&i(),s.$el.empty().append(n.success(t)),parent.Galaxy&&parent.Galaxy.currHistoryPanel&&parent.Galaxy.currHistoryPanel.refreshContents()},error:function(t){if(i&&i(),Galaxy.emit.debug("tool-form::submit","Submission failed.",t),t&&t.err_data){var e=s.data.matchResponse(t.err_data);for(var a in e){s.highlight(a,e[a]);break}}else s.modal.show({title:"Job submission failed",body:t&&t.err_msg||n.error(o),buttons:{Close:function(){s.modal.hide()}}})}})):(Galaxy.emit.debug("tool-form::submit()","Submission canceled. Validation failed."),void(i&&i()))},validate:function(t){var e=t.inputs,i=-1,n=null;for(var s in e){var o=e[s],a=this.data.match(s),r=this.field_list[a],l=this.input_list[a];if(a&&l&&r){if(!l.optional&&null==o)return this.highlight(a),!1;if(o&&o.batch){var c=o.values.length,d=c>0&&o.values[0]&&o.values[0].src;if(d)if(null===n)n=d;else if(n!==d)return this.highlight(a,"Please select either dataset or dataset list fields for all batch mode fields."),!1;if(-1===i)i=c;else if(i!==c)return this.highlight(a,"Please make sure that you select the same number of inputs for all batch mode fields. This field contains "+c+" selection(s) while a previous field contains "+i+"."),!1}}else Galaxy.emit.debug("tool-form::validate()","Retrieving input objects failed.")}return!0}});return{View:s}}.apply(e,n),!(void 0!==s&&(t.exports=s))},function(t,e,i){var n,s;(function(i){n=[],s=function(){return{help:function(t){var e=i("").addClass("ui-form-help").append(t.help);return e.find("a").attr("target","_blank"),
-e},success:function(t){if(!t.jobs||!t.jobs.length)return this.error(t);var e=t.jobs.length,i="";i=1==e?"1 job has":e+" jobs have";var n='
'+i+" been successfully added to the queue - resulting in the following datasets:
";for(var s in t.outputs)n+='
'+t.outputs[s].hid+": "+t.outputs[s].name+"
";return n+="
You can check the status of queued jobs and view the resulting data by refreshing the History pane. When the job has been run the status will change from 'running' to 'finished' if completed successfully or 'error' if problems were encountered.
"},error:function(t){return'
The server could not complete the request. Please contact the Galaxy Team if this error persists.
"},requirements:function(t){var e="This tool requires ";for(var i in t.requirements){var n=t.requirements[i];e+=n.name,n.version&&(e+=" (Version "+n.version+")"),ihere for more information.'}}}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(1))},function(t,e,i){var n,s;(function(o,a){n=[i(4),i(9),i(7)],s=function(t,e,i){var n=o.View.extend({initialize:function(t){var n=this;this.options=t,this.name=t.name||"element",this.multiple=t.multiple||!1,this.message=new i.Message,this.portlet=new e.View({cls:"ui-portlet-section"}),this.select=new i.Select.View({optional:t.optional}),this.button=new i.ButtonIcon({icon:"fa fa-sign-in",floating:"left",tooltip:"Insert new "+this.name,onclick:function(){n.add({id:n.select.value(),name:n.select.text()})}}),this.setElement(this._template(t)),this.$(".ui-list-message").append(this.message.$el),this.$(".ui-list-portlet").append(this.portlet.$el),this.$(".ui-list-button").append(this.button.$el),this.$(".ui-list-select").append(this.select.$el)},value:function(t){if(void 0!==t){if(this.portlet.empty(),a.isArray(t))for(var e in t){var i=t[e],n=null,s=null;"string"!=a.type(i)?(n=i.id,s=i.name):n=s=i,null!=n&&this.add({id:n,name:s})}this._refresh()}var o=[];return this.$(".ui-list-id").each(function(){o.push({id:a(this).prop("id"),name:a(this).find(".ui-list-name").html()})}),0==o.length?null:o},add:function(e){var i=this;if(0===this.$('[id="'+e.id+'"]').length)if(t.validate(e.id)){var n=a(this._templateRow({id:e.id,name:e.name}));n.on("click",function(){n.remove(),i._refresh()}),n.on("mouseover",function(){n.addClass("portlet-highlight")}),n.on("mouseout",function(){n.removeClass("portlet-highlight")}),this.portlet.append(n),this._refresh()}else this.message.update({message:"Please select a valid "+this.name+".",status:"danger"});else this.message.update({message:"This "+this.name+" is already in the list."})},update:function(t){this.select.update(t)},_refresh:function(){this.$(".ui-list-id").length>0?(!this.multiple&&this.button.disable(),this.$(".ui-list-portlet").show()):(this.button.enable(),this.$(".ui-list-portlet").hide()),this.options.onchange&&this.options.onchange()},_template:function(t){return'
'},_templateRow:function(t){return'
'+t.name+"
"}});return{View:n}}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(2),i(1))},function(t,e,i){var n,s;(function(o,a){n=[i(4),i(13)],s=function(t,e){var i=o.View.extend({initialize:function(i){var n=this;this.options=t.merge(i,{visible:!0,data:[],id:t.uid(),error_text:"No options available.",wait_text:"Please wait...",multiple:!1}),this.setElement(''),this.$message=a(""),this.$options=a(this._template(i)),this.$menu=a(''),this.$el.append(this.$message),this.$el.append(this.$menu),this.$el.append(this.$options),this.options.multiple&&(this.all_button=new e.ButtonCheck({onclick:function(){n.$("input").prop("checked",0!==n.all_button.value()),n.trigger("change")}}),this.$menu.append(this.all_button.$el)),this.options.visible||this.$el.hide(),this.update(this.options.data),void 0!==this.options.value&&this.value(this.options.value),this.on("change",function(){this.options.onchange&&this.options.onchange(this.value())})},update:function(t){var e=this._getValue();if(this.$options.empty(),this._templateOptions)this.$options.append(this._templateOptions(t));else for(var i in t){var n=a(this._templateOption(t[i]));n.addClass("ui-option"),n.tooltip({title:t[i].tooltip,placement:"bottom"}),this.$options.append(n)}var s=this;this.$("input").on("change",function(){s.value(s._getValue()),s.trigger("change")}),this.value(e),this.unwait()},value:function(t){if(void 0!==t&&(this.$("input").prop("checked",!1),null!==t)){t instanceof Array||(t=[t]);for(var e in t)this.$('input[value="'+t[e]+'"]').first().prop("checked",!0)}var i=this._getValue();if(this.all_button){var n=i;n=n instanceof Array?n.length:0,this.all_button.value(n,this._size())}return i},exists:function(t){if(void 0!==t){t instanceof Array||(t=[t]);for(var e in t)if(this.$('input[value="'+t[e]+'"]').length>0)return!0}return!1},first:function(){var t=this.$("input").first();return t.length>0?t.val():null},wait:function(){0==this._size()&&(this._messageShow(this.options.wait_text,"info"),this.$options.hide(),this.$menu.hide())},unwait:function(){var t=this._size();0==t?(this._messageShow(this.options.error_text,"danger"),this.$options.hide(),this.$menu.hide()):(this._messageHide(),this.$options.css("display","inline-block"),this.$menu.show())},_getValue:function(){var e=[];return this.$(":checked").each(function(){e.push(a(this).val())}),t.validate(e)?this.options.multiple?e:e[0]:null},_size:function(){return this.$(".ui-option").length},_messageShow:function(t,e){this.$message.show(),this.$message.removeClass(),this.$message.addClass("ui-message alert alert-"+e),this.$message.html(t)},_messageHide:function(){this.$message.hide()},_template:function(){return''}}),n=i.extend({_templateOption:function(e){var i=t.uid();return'"}}),s={};s.View=n.extend({initialize:function(t){t.type="radio",n.prototype.initialize.call(this,t)}});var r={};r.View=n.extend({initialize:function(t){t.multiple=!0,t.type="checkbox",n.prototype.initialize.call(this,t)}});var l={};return l.View=i.extend({initialize:function(t){i.prototype.initialize.call(this,t)},value:function(t){return void 0!==t&&(this.$("input").prop("checked",!1),this.$("label").removeClass("active"),this.$('[value="'+t+'"]').prop("checked",!0).closest("label").addClass("active")),this._getValue()},_templateOption:function(t){var e="fa "+t.icon;t.label||(e+=" no-padding");var i='"},_template:function(){return''}}),{Base:i,BaseIcons:n,Radio:s,RadioButton:l,Checkbox:r}}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(2),i(1))},function(t,e,i){var n,s;(function(o,a,r){n=[i(4),i(13)],s=function(t,e){var i=o.View.extend({optionsDefault:{id:t.uid(),cls:"ui-select",error_text:"No options available",empty_text:"Nothing selected",visible:!0,wait:!1,multiple:!1,searchable:!0,optional:!1},initialize:function(i){var n=this;this.model=new o.Model,this.options=t.merge(i,this.optionsDefault),this.setElement(this._template(this.options)),this.$select=this.$el.find(".select"),this.$icon_dropdown=this.$el.find(".icon-dropdown"),this.$icon_dropdown.on("click",function(){n.$select.select2&&n.$select.select2("open")});var s=null;this.$(".icon-resize").on("mousedown",function(t){var e=t.pageY,i=n.$select.height();s=s||i,a("#dd-helper").show().on("mousemove",function(t){n.$select.height(Math.max(i+(t.pageY-e),s))}).on("mouseup mouseleave",function(){a("#dd-helper").hide().off()})}),this.options.multiple&&(this.options.searchable?(this.all_button=new e.ButtonCheck({onclick:function(){var t=[];0!==n.all_button.value()&&(t=n._availableOptions()),n.value(t),n.trigger("change")}}),this.$el.prepend(this.all_button.$el)):this.$el.addClass("ui-select-multiple"),this.$select.prop("multiple",!0),this.$icon_dropdown.remove()),this.update(this.options.data),void 0!==this.options.value&&this.value(this.options.value),this.options.visible||this.hide(),this.options.wait?this.wait():this.show(),this.$select.on("change",function(){n.trigger("change")}),this.on("change",function(){n.options.onchange&&n.options.onchange(this.value())})},value:function(t){void 0!==t&&(null===t&&(t="__null__"),(this.exists(t)||this.options.multiple)&&(this.$select.val(t),this.$select.select2&&this.$select.select2("val",t)));var e=this._getValue();return this.all_button&&this.all_button.value(a.isArray(e)&&e.length||0,this._size()),e},first:function(){var t=this.$select.find("option").first();return t.length>0?t.val():null},text:function(){return this.$select.find("option:selected").text()},show:function(){this.unwait(),this.$select.show(),this.$el.show()},hide:function(){this.$el.hide()},wait:function(){this.$icon_dropdown.removeClass(),this.$icon_dropdown.addClass("icon-dropdown fa fa-spinner fa-spin")},unwait:function(){this.$icon_dropdown.removeClass(),this.$icon_dropdown.addClass("icon-dropdown fa fa-caret-down")},disabled:function(){return this.$select.is(":disabled")},enable:function(){this.$select.prop("disabled",!1)},disable:function(){this.$select.prop("disabled",!0)},add:function(t,e){r.each(this.model.get("options"),function(e){!r.findWhere(t,e)&&t.push(e)}),e&&t&&t.sort(e),this.update(t)},update:function(t){var e=this._getValue();this.$select.find("option").remove(),!this.options.multiple&&this.options.optional&&this.$select.append(this._templateOption({value:"__null__",label:this.options.empty_text})),this.model.set("options",t);for(var i in t)this.$select.append(this._templateOption(t[i]));0==this._size()?(this.disable(),this.$select.append(this._templateOption({value:"__null__",label:this.options.error_text}))):this.enable(),this.options.searchable&&(this.$select.select2("destroy"),this.$select.select2({closeOnSelect:!this.options.multiple}),this.$(".select2-container .select2-search input").off("blur")),this.value(e),null!==this._getValue()||this.options.multiple&&this.options.optional||this.value(this.first())},setOnChange:function(t){this.options.onchange=t},exists:function(t){return this.$select.find('option[value="'+t+'"]').length>0},_getValue:function(){var e=this.$select.val();return t.validate(e)?e:null},_availableOptions:function(){var t=[];return this.$select.find("option").each(function(e,i){t.push(a(i).attr("value"))}),t},_size:function(){return this.$select.find("option").length},_templateOption:function(t){return'"},_template:function(t){return'
',"Warning: This is a experimental feature. Most Galaxy tools will not annotate"," citations explicitly at this time. When writing up your analysis, please manually"," review your histories and find all references"," that should be cited in order to completely describe your work. Also, please remember to",' cite Galaxy.',"
"],"element");return o.extend({},e.DatasetListItemView.prototype.templates,{titleBar:t})}();var u=c.extend({className:c.prototype.className+" dataset-collection-element",_swapNewRender:function(t){c.prototype._swapNewRender.call(this,t);var e=this.model.get("state")||"ok";return this.$el.addClass("state-"+e),this.$el},toString:function(){var t=this.model?this.model+"":"(no model)";return"NestedDCDCEListItemView("+t+")"}});return{DCListItemView:c,DCEListItemView:d,DatasetDCEListItemView:h,NestedDCDCEListItemView:u}}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(3),i(1),i(1))},function(t,e,i){var n,s;(function(o,a,r){n=[i(71),i(6),i(5)],s=function(t,e,i){"use strict";var n={defaults:{model_class:"DatasetCollectionElement",element_identifier:null,element_index:null,element_type:null},_mergeObject:function(t){return o.extend(t,t.object,{element_id:t.id}),delete t.object,t},constructor:function(t,e){t=this._mergeObject(t),this.idAttribute="element_id",a.Model.apply(this,arguments)},parse:function(t,e){var i=t;return i=this._mergeObject(i)}},s=a.Model.extend(e.LoggableMixin).extend(n).extend({_logNamespace:"collections"}),l=a.Collection.extend(e.LoggableMixin).extend({_logNamespace:"collections",model:s,toString:function(){return["DatasetCollectionElementCollection(",this.length,")"].join("")}}),c=t.DatasetAssociation.extend(e.mixin(n,{url:function(){return this.has("history_id")?Galaxy.root+"api/histories/"+this.get("history_id")+"/contents/"+this.get("id"):(console.warn("no endpoint for non-hdas within a collection yet"),Galaxy.root+"api/datasets")},defaults:o.extend({},t.DatasetAssociation.prototype.defaults,n.defaults),constructor:function(t,e){this.debug(" DatasetDCE.constructor:",t,e),n.constructor.call(this,t,e)},hasDetails:function(){return this.elements&&this.elements.length},toString:function(){var t=this.get("element_identifier");return["DatasetDCE(",t,")"].join("")}})),d=l.extend({model:c,toString:function(){return["DatasetDCECollection(",this.length,")"].join("")}}),h=a.Model.extend(e.LoggableMixin).extend(e.SearchableModelMixin).extend({_logNamespace:"collections",defaults:{collection_type:null,deleted:!1},collectionClass:l,initialize:function(t,e){this.debug(this+"(DatasetCollection).initialize:",t,e,this),this.elements=this._createElementsModel(),this.on("change:elements",function(){this.log("change:elements"),this.elements=this._createElementsModel()})},_createElementsModel:function(){this.debug(this+"._createElementsModel",this.collectionClass,this.get("elements"),this.elements);var t=this.get("elements")||[];return this.unset("elements",{silent:!0}),this.elements=new this.collectionClass(t),this.elements},toJSON:function(){var t=a.Model.prototype.toJSON.call(this);return this.elements&&(t.elements=this.elements.toJSON()),t},inReadyState:function(){var t=this.get("populated");return this.isDeletedOrPurged()||t},hasDetails:function(){return this.has("annotation")},getVisibleContents:function(t){return this.elements},parse:function(t,e){var i=a.Model.prototype.parse.call(this,t,e);return i.create_time&&(i.create_time=new Date(i.create_time)),i.update_time&&(i.update_time=new Date(i.update_time)),i},"delete":function(t){return this.get("deleted")?r.when():this.save({deleted:!0},t)},undelete:function(t){return!this.get("deleted")||this.get("purged")?r.when():this.save({deleted:!1},t)},isDeletedOrPurged:function(){return this.get("deleted")||this.get("purged")},searchAttributes:["name"],toString:function(){var t=[this.get("id"),this.get("name")||this.get("element_identifier")];return"DatasetCollection("+t.join(",")+")"}}),u=h.extend({collectionClass:d,toString:function(){return"List"+h.prototype.toString.call(this)}}),p=u.extend({toString:function(){return"Pair"+h.prototype.toString.call(this)}}),g=h.extend(e.mixin(n,{constructor:function(t,e){this.debug(" NestedDCDCE.constructor:",t,e),n.constructor.call(this,t,e)},toString:function(){var t=this.object?""+this.object:this.get("element_identifier");return["NestedDCDCE(",t,")"].join("")}})),f=l.extend({model:g,toString:function(){return["NestedDCDCECollection(",this.length,")"].join("")}}),m=p.extend(e.mixin(n,{constructor:function(t,e){this.debug(" NestedPairDCDCE.constructor:",t,e),n.constructor.call(this,t,e)},toString:function(){var t=this.object?""+this.object:this.get("element_identifier");return["NestedPairDCDCE(",t,")"].join("")}})),v=f.extend({model:m,toString:function(){return["NestedPairDCDCECollection(",this.length,")"].join("")}}),_=h.extend({collectionClass:v,toString:function(){return["ListPairedDatasetCollection(",this.get("name"),")"].join("")}});return{ListDatasetCollection:u,PairDatasetCollection:p,ListPairedDatasetCollection:_}}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(3),i(2),i(1))},function(t,e,i){var n,s;(function(o,a,r,l){n=[i(40),i(12),i(6),i(8),i(87),i(5),i(84)],s=function(t,e,i,n,s,c){"use strict";function d(t){var e=t.toJSON(),i=f(e,{creationFn:function(e,i){return e=e.map(function(t){return{id:t.id,name:t.name,src:"dataset"===t.history_content_type?"hda":"hdca"}}),t.createHDCA(e,"list",i)}});return i}var h="collections",u=o.View.extend(i.LoggableMixin).extend({_logNamespace:h,tagName:"li",className:"collection-element",initialize:function(t){this.element=t.element||{},this.selected=t.selected||!1},render:function(){return this.$el.attr("data-element-id",this.element.id).attr("draggable",!0).html(this.template({element:this.element})),this.selected&&this.$el.addClass("selected"),this},template:a.template(['',"<%- element.name %>","",'"].join("")),select:function(t){this.$el.toggleClass("selected",t),this.trigger("select",{source:this,selected:this.$el.hasClass("selected")})},discard:function(){var t=this,e=this.$el.parent().width();this.$el.animate({"margin-right":e},"fast",function(){t.trigger("discard",{source:t}),t.destroy()})},destroy:function(){this.off(),this.$el.remove()},events:{click:"_click","click .name":"_clickName","click .discard":"_clickDiscard",dragstart:"_dragstart",dragend:"_dragend",dragover:"_sendToParent",drop:"_sendToParent"},_click:function(t){t.stopPropagation(),this.select(t)},_clickName:function(t){t.stopPropagation(),t.preventDefault();var e=([c("Enter a new name for the element"),":\n(",c("Note that changing the name here will not rename the dataset"),")"].join(""),prompt(c("Enter a new name for the element")+":",this.element.name));e&&(this.element.name=e,this.render())},_clickDiscard:function(t){t.stopPropagation(),this.discard()},_dragstart:function(t){t.originalEvent&&(t=t.originalEvent),t.dataTransfer.effectAllowed="move",t.dataTransfer.setData("text/plain",JSON.stringify(this.element)),this.$el.addClass("dragging"),this.$el.parent().trigger("collection-element.dragstart",[this])},_dragend:function(t){this.$el.removeClass("dragging"),this.$el.parent().trigger("collection-element.dragend",[this])},_sendToParent:function(t){this.$el.parent().trigger(t)},toString:function(){return"DatasetCollectionElementView()"}}),p=o.View.extend(i.LoggableMixin).extend({_logNamespace:h,elementViewClass:u,collectionClass:t.HistoryListDatasetCollection,className:"list-collection-creator collection-creator flex-row-container",minElements:1,defaultAttributes:{creationFn:function(){throw new TypeError("no creation fn for creator")},oncreate:function(){},oncancel:function(){},autoscrollDist:24,highlightClr:"rgba( 64, 255, 255, 1.0 )"},initialize:function(t){this.metric("ListCollectionCreator.initialize",t);var e=this;a.each(this.defaultAttributes,function(i,n){i=t[n]||i,e[n]=i}),e.initialElements=t.elements||[],this._instanceSetUp(),this._elementsSetUp(),this._setUpBehaviors()},_instanceSetUp:function(){this.selectedIds={},this.$dragging=null,this.blocking=!1},_elementsSetUp:function(){this.invalidElements=[],this.workingElements=[],this.elementViews=[],this.workingElements=this.initialElements.slice(0),this._ensureElementIds(),this._validateElements(),this._mangleDuplicateNames(),this._sortElements()},_ensureElementIds:function(){return this.workingElements.forEach(function(t){t.hasOwnProperty("id")||(t.id=a.uniqueId())}),this.workingElements},_validateElements:function(){var t=this;return t.invalidElements=[],this.workingElements=this.workingElements.filter(function(e){var i=t._isElementInvalid(e);return i&&t.invalidElements.push({element:e,text:i}),!i}),this.workingElements},_isElementInvalid:function(t){return"dataset"!==t.history_content_type?c("is not a dataset"):t.state!==e.OK?c(a.contains(e.NOT_READY_STATES,t.state)?"hasn't finished running yet":"has errored, is paused, or is not accessible"):t.deleted||t.purged?c("has been deleted or purged"):null},_mangleDuplicateNames:function(){var t=900,e=1,i={};this.workingElements.forEach(function(n){for(var s=n.name;i.hasOwnProperty(s);)if(s=n.name+" ("+e+")",e+=1,e>=t)throw new Error("Safety hit in while loop - thats impressive");n.name=s,i[n.name]=!0})},_sortElements:function(t){},render:function(t,e){return this.workingElements.length .clear-selected").show():this.$(".collection-elements-controls > .clear-selected").hide()},_renderList:function(t,e){var i=this,n=l(""),s=i.$list();a.each(this.elementViews,function(t){t.destroy(),i.removeElementView(t)}),i.workingElements.forEach(function(t){var e=i._createElementView(t);n.append(e.$el)}),i._renderClearSelected(),s.empty().append(n.children()),a.invoke(i.elementViews,"render"),s.height()>s.css("max-height")?s.css("border-width","1px 0px 1px 0px"):s.css("border-width","0px")},_createElementView:function(t){var e=new this.elementViewClass({element:t,selected:a.has(this.selectedIds,t.id)});return this.elementViews.push(e),this._listenToElementView(e),e},_listenToElementView:function(t){var e=this;e.listenTo(t,{select:function(t){var i=t.source.element;t.selected?e.selectedIds[i.id]=!0:delete e.selectedIds[i.id],e.trigger("elements:select",t)},discard:function(t){e.trigger("elements:discard",t)}})},addElementView:function(t){},removeElementView:function(t){delete this.selectedIds[t.element.id],this._renderClearSelected(),this.elementViews=a.without(this.elementViews,t),this.stopListening(t)},_renderNoElementsLeft:function(){this._disableNameAndCreate(!0),this.$(".collection-elements").append(this.templates.noElementsLeft())},_elementToJSON:function(t){return t},createList:function(t){if(!this.workingElements.length){var e=c("No valid elements for final list")+". ";return e+=''+c("Cancel")+" ",e+=c("or"),e+=' '+c("start over")+".",void this._showAlert(e)}var i=this,n=this.workingElements.map(function(t){return i._elementToJSON(t)});return i.blocking=!0,i.creationFn(n,t).always(function(){i.blocking=!1}).fail(function(t,e,n){i.trigger("error",{xhr:t,status:e,message:c("An error occurred while creating this collection")})}).done(function(t,e,n){i.trigger("collection:created",t,e,n),i.metric("collection:created",t),"function"==typeof i.oncreate&&i.oncreate.call(this,t,e,n)})},_setUpBehaviors:function(){return this.on("error",this._errorHandler),this.once("rendered",function(){this.trigger("rendered:initial",this)}),this.on("elements:select",function(t){this._renderClearSelected()}),this.on("elements:discard",function(t){var e=t.source.element;this.removeElementView(t.source),this.workingElements=a.without(this.workingElements,e),this.workingElements.length||this._renderNoElementsLeft()}),this},_errorHandler:function(t){this.error(t);var e=this;if(content=t.message||c("An error occurred"),t.xhr){var i=t.xhr,n=t.message;0===i.readyState&&0===i.status?content+=": "+c("Galaxy could not be reached and may be updating.")+c(" Try again in a few minutes."):i.responseJSON?content+=":
",c(["Collections of datasets are permanent, ordered lists of datasets that can be passed to tools and ","workflows in order to have analyses done on each member of the entire group. This interface allows ","you to create a collection and re-order the final collection."].join("")),"
","
","
",c(["Rename elements in the list by clicking on ",'the existing name.'].join("")),"
","
",c(["Discard elements from the final created list by clicking on the ",'"Discard" button.'].join("")),"
","
",c(["Reorder the list by clicking and dragging elements. Select multiple elements by clicking on ",'them and you can then move those selected by dragging the ',"entire group. Deselect them by clicking them again or by clicking the ",'the "Clear selected" link.'].join("")),"
","
",c(['Click the "Start over" link to begin again as if you had just opened ',"the interface."].join("")),"
","
",c(['Click the "Cancel" button to exit the interface.'].join("")),"
","
","
",c(['Once your collection is complete, enter a name and ','click "Create list".'].join("")),"
"].join("")),invalidElements:a.template([c("The following selections could not be included due to problems:"),"
','',"<% if( _.size( problems ) ){ %>",c("The following selections could not be included due to problems"),":","
<% _.each( problems, function( problem ){ %>","
<%- problem.element.name %>: <%- problem.text %>
","<% }); %>
","<% } else if( _.size( elements ) < 1 ){ %>",c("No datasets were selected"),".","<% } %>"," ",c("At least one element is needed for the collection"),". ",c("You may need to "),'',c("cancel")," ",c("and reselect new elements"),".","","
","
",'"].join(""))},toString:function(){return"ListCollectionCreator"}}),g=function(t,e,i){var s,o=l.Deferred(),r=Galaxy.modal||new n.View;return e=a.defaults(e||{},{elements:t,oncancel:function(){r.hide(),o.reject("cancelled")},oncreate:function(t,e){r.hide(),o.resolve(e)}}),s=new i(e),r.show({title:e.title||c("Create a collection"),body:s.$el,width:"80%",height:"100%",closing_events:!0}),s.render(),window._collectionCreator=s,o},f=function(t,e){return e=e||{},e.title=c("Create a collection from a list of datasets"),g(t,e,p)};return{DatasetCollectionElementView:u,ListCollectionCreator:p,collectionCreatorModal:g,listCollectionCreatorModal:f,createListCollection:d}}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(2),i(3),i(1),i(1))},function(t,e,i){var n,s;(function(o,a,r,l){n=[i(43),i(12),i(24),i(6),i(5)],s=function(t,e,i,n,s){"use strict";var c="dataset",d=t.ListItemView,h=d.extend({_logNamespace:c,className:d.prototype.className+" dataset",id:function(){return["dataset",this.model.get("id")].join("-")},initialize:function(t){t.logger&&(this.logger=this.model.logger=t.logger),this.log(this+".initialize:",t),d.prototype.initialize.call(this,t),this.linkTarget=t.linkTarget||"_blank"},_setUpListeners:function(){d.prototype._setUpListeners.call(this),this.listenTo(this.model,"change",function(t,e){this.model.changedAttributes().state&&this.model.inReadyState()&&this.expanded&&!this.model.hasDetails()?this.model.fetch():this.render()})},_fetchModelDetails:function(){var t=this;return t.model.inReadyState()&&!t.model.hasDetails()?t.model.fetch({silent:!0}):o.when()},remove:function(t,e){var i=this;t=t||this.fxSpeed,this.$el.fadeOut(t,function(){a.View.prototype.remove.call(i),e&&e.call(i)})},render:function(t){return d.prototype.render.call(this,t)},_swapNewRender:function(t){return d.prototype._swapNewRender.call(this,t),this.model.has("state")&&this.$el.addClass("state-"+this.model.get("state")),this.$el},_renderPrimaryActions:function(){return[this._renderDisplayButton()]},_renderDisplayButton:function(){var t=this.model.get("state");
-if(t===e.NOT_VIEWABLE||t===e.DISCARDED||!this.model.get("accessible"))return null;var n={target:this.linkTarget,classes:"display-btn"};if(this.model.get("purged"))n.disabled=!0,n.title=s("Cannot display datasets removed from disk");else if(t===e.UPLOAD)n.disabled=!0,n.title=s("This dataset must finish uploading before it can be viewed");else if(t===e.NEW)n.disabled=!0,n.title=s("This dataset is not yet viewable");else{n.title=s("View data"),n.href=this.model.urls.display;var o=this;n.onclick=function(t){Galaxy.frame&&Galaxy.frame.active&&(Galaxy.frame.addDataset(o.model.get("id")),t.preventDefault())}}return n.faIcon="fa-eye",i(n)},_renderDetails:function(){if(this.model.get("state")===e.NOT_VIEWABLE)return r(this.templates.noAccess(this.model.toJSON(),this));var t=d.prototype._renderDetails.call(this);return t.find(".actions .left").empty().append(this._renderSecondaryActions()),t.find(".summary").html(this._renderSummary()).prepend(this._renderDetailMessages()),t.find(".display-applications").html(this._renderDisplayApplications()),this._setUpBehaviors(t),t},_renderSummary:function(){var t=this.model.toJSON(),e=this.templates.summaries[t.state];return(e=e||this.templates.summaries.unknown)(t,this)},_renderDetailMessages:function(){var t=this,e=r(''),i=t.model.toJSON();return l.each(t.templates.detailMessages,function(n){e.append(r(n(i,t)))}),e},_renderDisplayApplications:function(){return this.model.isDeletedOrPurged()?"":[this.templates.displayApplications(this.model.get("display_apps"),this),this.templates.displayApplications(this.model.get("display_types"),this)].join("")},_renderSecondaryActions:function(){switch(this.debug("_renderSecondaryActions"),this.model.get("state")){case e.NOT_VIEWABLE:return[];case e.OK:case e.FAILED_METADATA:case e.ERROR:return[this._renderDownloadButton(),this._renderShowParamsButton()]}return[this._renderShowParamsButton()]},_renderShowParamsButton:function(){return i({title:s("View details"),classes:"params-btn",href:this.model.urls.show_params,target:this.linkTarget,faIcon:"fa-info-circle"})},_renderDownloadButton:function(){return this.model.get("purged")||!this.model.hasData()?null:l.isEmpty(this.model.get("meta_files"))?r(['','',""].join("")):this._renderMetaFileDownloadButton()},_renderMetaFileDownloadButton:function(){var t=this.model.urls;return r(['
This Galaxy server allows you to upload files via FTP. To upload some files, log in to the FTP server at '+this.options.ftp_upload_site+' using your Galaxy credentials (email address and password).
'}})}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(2),i(1))},function(t,e,i){var n,s;(function(o,a,r){n=[i(4)],s=function(t){return o.View.extend({options:{class_check:"fa-check-square-o",class_uncheck:"fa-square-o",parameters:[{id:"space_to_tab",title:"Convert spaces to tabs"},{id:"to_posix_lines",title:"Use POSIX standard"}]},initialize:function(t){this.model=t.model,this.setElement(a("").addClass("upload-settings")),this.$el.append(a("").addClass("upload-settings-cover")),this.$el.append(a("
"],"history");return o.extend(o.clone(p.prototype.templates),{controls:t})}(),{HistoryView:g}}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(3),i(1),i(1))},function(t,e,i){var n,s;(function(o,a){n=[i(79),i(104),i(6),i(5)],s=function(t,e,i,n){"use strict";function s(t,e,i){return a.clone(r).filter(function(n){return t&&!n.anon?!1:!e&&n.purge?!1:(n.href&&(n.href=i+n.href,n.target="galaxy_main"),n.confirm&&(n.func=function(){confirm(n.confirm)&&(galaxy_main.location=n.href)}),!0)})}var r=[{html:n("History Lists"),header:!0},{html:n("Saved Histories"),href:"history/list"},{html:n("Histories Shared with Me"),href:"history/list_shared"},{html:n("History Actions"),header:!0,anon:!0},{html:n("Create New"),func:function(){Galaxy&&Galaxy.currHistoryPanel&&Galaxy.currHistoryPanel.createNewHistory()}},{html:n("Copy History"),func:function(){e(Galaxy.currHistoryPanel.model).done(function(){Galaxy.currHistoryPanel.loadCurrentHistory()})}},{html:n("Share or Publish"),href:"history/sharing"},{html:n("Show Structure"),href:"history/display_structured",anon:!0},{html:n("Extract Workflow"),href:"workflow/build_from_current_history"},{html:n("Delete"),confirm:n("Really delete the current history?"),href:"history/delete_current"},{html:n("Delete Permanently"),confirm:n("Really delete the current history permanently? This cannot be undone."),href:"history/delete_current?purge=True",purge:!0,anon:!0},{html:n("Dataset Actions"),header:!0,anon:!0},{html:n("Copy Datasets"),href:"dataset/copy_datasets"},{html:n("Dataset Security"),href:"root/history_set_default_permissions"},{html:n("Resume Paused Jobs"),href:"history/resume_paused_jobs?current=True",anon:!0},{html:n("Collapse Expanded Datasets"),func:function(){Galaxy&&Galaxy.currHistoryPanel&&Galaxy.currHistoryPanel.collapseAll()}},{html:n("Unhide Hidden Datasets"),anon:!0,func:function(){if(Galaxy&&Galaxy.currHistoryPanel&&confirm(n("Really unhide all hidden datasets?"))){var t=Galaxy.currHistoryPanel.model.contents.hidden();t.ajaxQueue(o.Model.prototype.save,{visible:!0}).done(function(){Galaxy.currHistoryPanel.renderItems()}).fail(function(){alert("There was an error unhiding the datasets"),console.error(arguments)})}}},{html:n("Delete Hidden Datasets"),anon:!0,func:function(){if(Galaxy&&Galaxy.currHistoryPanel&&confirm(n("Really delete all hidden datasets?"))){var t=Galaxy.currHistoryPanel.model.contents.hidden();t.ajaxQueue(o.Model.prototype.save,{deleted:!0,visible:!0}).done(function(){Galaxy.currHistoryPanel.renderItems()}).fail(function(){alert("There was an error deleting the datasets"),console.error(arguments)})}}},{html:n("Purge Deleted Datasets"),confirm:n("Really delete all deleted datasets permanently? This cannot be undone."),href:"history/purge_deleted_datasets",purge:!0,anon:!0},{html:n("Downloads"),header:!0},{html:n("Export Tool Citations"),href:"history/citations",anon:!0},{html:n("Export History to File"),href:"history/export_archive?preview=True",anon:!0},{html:n("Other Actions"),header:!0},{html:n("Import from File"),href:"history/import_archive"}],l=function(e,i){i=i||{};var n=void 0===i.anonymous?!0:i.anonymous,o=i.purgeAllowed||!1,a=s(n,o,Galaxy.root);return new t(e,a)};return l}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(2),i(3))},function(t,e,i){var n,s;(function(o,a,r){n=[i(4)],s=function(t){var e=o.View.extend({optionsDefault:{title_new:"",operations:null,onnew:null,max:null,onchange:null},initialize:function(e){this.visible=!1,this.$nav=null,this.$content=null,this.first_tab=null,this.current_id=null,this.options=t.merge(e,this.optionsDefault);var i=a(this._template(this.options));this.$nav=i.find(".tab-navigation"),this.$content=i.find(".tab-content"),this.setElement(i),this.list={};var n=this;if(this.options.operations&&a.each(this.options.operations,function(t,e){e.$el.prop("id",t),n.$nav.find(".operations").append(e.$el)}),this.options.onnew){var s=a(this._template_tab_new(this.options));this.$nav.append(s),s.tooltip({title:"Add a new tab",placement:"bottom",container:n.$el}),s.on("click",function(t){s.tooltip("hide"),n.options.onnew()})}},size:function(){return r.size(this.list)},current:function(){return this.$el.find(".tab-pane.active").attr("id")},add:function(t){var e=this,i=t.id,n=a(this._template_tab(t)),s=a(this._template_tab_content(t));if(this.list[i]=!!t.ondel,this.options.onnew?this.$nav.find("#new-tab").before(n):this.$nav.append(n),s.append(t.$el),this.$content.append(s),1==this.size()&&(n.addClass("active"),s.addClass("active"),this.first_tab=i),this.options.max&&this.size()>=this.options.max&&this.$el.find("#new-tab").hide(),t.ondel){var o=n.find("#delete");o.tooltip({title:"Delete this tab",placement:"bottom",container:e.$el}),o.on("click",function(){return o.tooltip("destroy"),e.$el.find(".tooltip").remove(),t.ondel(),!1})}n.on("click",function(n){n.preventDefault(),t.onclick?t.onclick():e.show(i)}),this.current_id||(this.current_id=i)},del:function(t){this.$el.find("#tab-"+t).remove(),this.$el.find("#"+t).remove(),this.first_tab==t&&(this.first_tab=null),null!=this.first_tab&&this.show(this.first_tab),this.list[t]&&delete this.list[t],this.size()
"},_template_tab_content:function(t){return''}});return{View:e}}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(2),i(1),i(3))},function(t,e,i){var n,s;(function(o,a){n=[i(4),i(81),i(80),i(26),i(7),i(27),i(88)],s=function(t,e,i,n,s,r){return o.View.extend({status_classes:{init:"upload-mode fa fa-exclamation text-primary",ready:"upload-mode fa fa-check text-success",running:"upload-mode fa fa-spinner fa-spin",success:"upload-mode fa fa-check",error:"upload-mode fa fa-exclamation-triangle"},initialize:function(t,e){this.app=t;var i=this;this.model=e.model,this.setElement(this._template(e.model)),this.uploadinput=this.$el.uploadinput({ondragover:function(){i.model.get("enabled")&&i.$el.addClass("warning")},ondragleave:function(){i.$el.removeClass("warning")},onchange:function(t){"running"!=i.model.get("status")&&t&&t.length>0&&(i.model.reset({file_data:t[0],file_name:t[0].name,file_size:t[0].size,file_mode:t[0].mode||"local"}),i._refreshReady())}}),this.button_menu=new s.ButtonMenu({icon:"fa-caret-down",title:"Select",pull:"left"}),this.$("#source").append(this.button_menu.$el),this.button_menu.addMenu({icon:"fa-laptop",title:"Choose local file",onclick:function(){i.uploadinput.dialog()}}),this.app.ftp_upload_site&&this.button_menu.addMenu({icon:"fa-folder-open-o",title:"Choose FTP file",onclick:function(){i._showFtp()}}),this.button_menu.addMenu({icon:"fa-edit",title:"Paste/Fetch data",onclick:function(){i.model.reset({file_mode:"new",file_name:"New File"})}}),this.ftp=new n.View({title:"Choose FTP file:",container:this.$("#source").find(".ui-button-menu"),placement:"right"}),this.settings=new n.View({title:"Upload configuration",container:this.$("#settings"),placement:"bottom"}),this.$("#text-content").on("change input",function(t){i.model.set("url_paste",a(t.target).val()),i.model.set("file_size",a(t.target).val().length),i._refreshReady()}),this.$("#settings").on("click",function(t){i._showSettings()}).on("mousedown",function(t){t.preventDefault()}),this.model.on("change:percentage",function(){i._refreshPercentage()}),this.model.on("change:status",function(){i._refreshStatus()}),this.model.on("change:info",function(){i._refreshInfo()}),this.model.on("change:file_name",function(){i._refreshFileName()}),this.model.on("change:file_mode",function(){i._refreshMode()}),this.model.on("change:file_size",function(){i._refreshFileSize()}),this.model.on("remove",function(){i.remove()}),this.app.collection.on("reset",function(){i.remove()})},render:function(){this.$("#file_name").html(this.model.get("file_name")||"-"),this.$("#file_desc").html(this.model.get("file_desc")||"Unavailable"),this.$("#file_size").html(t.bytesToString(this.model.get("file_size"))),this.$("#status").removeClass().addClass(this.status_classes.init)},remove:function(){o.View.prototype.remove.apply(this)},_refreshReady:function(){this.app.collection.each(function(t){t.set("status",t.get("file_size")>0&&"ready"||"init")})},_refreshMode:function(){var t=this.model.get("file_mode");"new"==t?(this.height=this.$el.height(),this.$("#text").css({width:this.$el.width()-16+"px",top:this.$el.height()-8+"px"}).show(),this.$el.height(this.$el.height()-8+this.$("#text").height()+16),this.$("#text-content").val("").trigger("keyup")):(this.$el.height(this.height),this.$("#text").hide())},_refreshInfo:function(){var t=this.model.get("info");t?this.$("#info-text").html("Failed: "+t).show():this.$("#info-text").hide()},_refreshPercentage:function(){var t=parseInt(this.model.get("percentage"));0!=t?this.$(".progress-bar").css({width:t+"%"}):(this.$(".progress-bar").addClass("no-transition"),this.$(".progress-bar").css({width:"0%"}),this.$(".progress-bar")[0].offsetHeight,this.$(".progress-bar").removeClass("no-transition")),100!=t?this.$("#percentage").html(t+"%"):this.$("#percentage").html("Adding to history...")},_refreshStatus:function(){var t=this.model.get("status");this.$("#status").removeClass().addClass(this.status_classes[t]),this.model.set("enabled","running"!=t),this.$("#text-content").attr("disabled",!this.model.get("enabled")),this.$el.removeClass("success danger warning"),"running"!=t&&"ready"!=t||this.model.set("percentage",0),"running"==t?this.$("#source").find(".button").addClass("disabled"):this.$("#source").find(".button").removeClass("disabled"),"success"==t&&(this.$el.addClass("success"),this.model.set("percentage",100),this.$("#percentage").html("100%")),"error"==t?(this.$el.addClass("danger"),this.model.set("percentage",0),this.$("#info-progress").hide(),this.$("#info-text").show()):(this.$("#info-progress").show(),this.$("#info-text").hide())},_refreshFileName:function(){this.$("#file_name").html(this.model.get("file_name")||"-")},_refreshFileSize:function(){this.$("#file_size").html(t.bytesToString(this.model.get("file_size")))},_showFtp:function(){if(this.ftp.visible)this.ftp.hide();else{this.ftp.empty();var t=this;this.ftp.append(new i({ftp_upload_site:this.app.ftp_upload_site,onchange:function(e){t.ftp.hide(),"running"!=t.model.get("status")&&e&&(t.model.reset({file_mode:"ftp",file_name:e.path,file_size:e.size,file_path:e.path}),t._refreshReady())}}).$el),this.ftp.show()}},_showSettings:function(){this.settings.visible?this.settings.hide():(this.settings.empty(),this.settings.append(new e(this).$el),this.settings.show())},_template:function(t){return'
You can tell Galaxy to download data from web by entering URL in this box (one per line). You can also directly paste the contents of a file.
0%
'}})}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(2),i(1))},function(t,e,i){var n,s;(function(o,a,r){n=[i(4),i(53),i(112),i(26),i(27),i(7)],s=function(t,e,i,n,s,l){return o.View.extend({select_extension:null,select_genome:null,collection:new e.Collection,initialize:function(t){this.app=t,this.options=t.options,this.list_extensions=t.list_extensions,this.list_genomes=t.list_genomes,this.ftp_upload_site=t.currentFtp();var e=this;this.setElement(this._template()),this.btnStart=new l.Button({title:"Start",onclick:function(){e._eventStart()}}),this.btnClose=new l.Button({title:"Close",onclick:function(){e.app.modal.hide()}});var i=[this.btnStart,this.btnClose];for(var n in i)this.$("#upload-buttons").prepend(i[n].$el);this.select_extension=new s.View({css:"footer-selection",container:this.$("#footer-extension"),data:a.filter(this.list_extensions,function(t){return t.composite_files}),onchange:function(t){e.collection.reset();var i=a.findWhere(e.list_extensions,{id:t});if(i&&i.composite_files)for(var n in i.composite_files){var s=i.composite_files[n];e.collection.add({id:e.collection.size(),file_desc:s.description||s.name})}}}),this.$("#footer-extension-info").on("click",function(t){e._showExtensionInfo({$el:r(t.target),title:e.select_extension.text(),extension:e.select_extension.value(),placement:"top"})}).on("mousedown",function(t){t.preventDefault()}),this.select_genome=new s.View({css:"footer-selection",container:this.$("#footer-genome"),data:this.list_genomes,value:this.options.default_genome}),this.collection.on("add",function(t){e._eventAnnounce(t)}),this.collection.on("change add",function(){e._updateScreen()}).trigger("change"),this.select_extension.options.onchange(this.select_extension.value())},_eventAnnounce:function(t){var e=new i(this,{model:t});this.$("#upload-table > tbody:first").append(e.$el),e.render(),this.collection.length>0?this.$("#upload-table").show():this.$("#upload-table").hide()},_eventStart:function(){var t=this;this.collection.each(function(e){e.set("genome",t.select_genome.value()),e.set("extension",t.select_extension.value())}),r.uploadpost({url:this.app.options.nginx_upload_path,data:this.app.toData(this.collection.filter()),success:function(e){t._eventSuccess(e)},error:function(e){t._eventError(e)},progress:function(e){t._eventProgress(e)}})},_eventProgress:function(t){this.collection.each(function(e){e.set("percentage",t)})},_eventSuccess:function(t){this.collection.each(function(t){t.set("status","success")}),Galaxy.currHistoryPanel.refreshContents()},_eventError:function(t){this.collection.each(function(e){e.set("status","error"),e.set("info",t)})},_showExtensionInfo:function(t){var e=t.$el,i=t.extension,s=t.title,o=a.findWhere(this.list_extensions,{id:i});this.extension_popup&&this.extension_popup.remove(),this.extension_popup=new n.View({placement:t.placement||"bottom",container:e,destroy:!0}),this.extension_popup.title(s),this.extension_popup.empty(),this.extension_popup.append(this._templateDescription(o)),this.extension_popup.show()},_updateScreen:function(){var t=this.collection.first();t&&"running"==t.get("status")?(this.select_genome.disable(),this.select_extension.disable()):(this.select_genome.enable(),this.select_extension.enable()),this.collection.where({status:"ready"}).length==this.collection.length&&this.collection.length>0?(this.btnStart.enable(),this.btnStart.$el.addClass("btn-primary")):(this.btnStart.disable(),this.btnStart.$el.removeClass("btn-primary")),this.collection.length>0?this.$("#upload-table").show():this.$("#upload-table").hide()},_templateDescription:function(t){if(t.description){var e=t.description;return t.description_url&&(e+=' (read more)'),e}return"There is no description available for this file extension."},_template:function(){return'
Description
Name
Size
Settings
Status
'}})}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(2),i(3),i(1))},function(t,e,i){var n,s;(function(o,a){n=[i(4),i(53),i(81),i(26),i(27)],s=function(t,e,i,n,s){return o.View.extend({status_classes:{init:"upload-icon-button fa fa-trash-o",queued:"upload-icon fa fa-spinner fa-spin",running:"upload-icon fa fa-spinner fa-spin",success:"upload-icon-button fa fa-check",error:"upload-icon-button fa fa-exclamation-triangle"},settings:null,select_genome:null,select_extension:null,initialize:function(t,e){this.app=t;var i=this;this.model=e.model,this.setElement(this._template(e.model)),this.settings=new n.View({title:"Upload configuration",container:this.$("#settings"),placement:"bottom"});var o=this.app.select_genome.value();this.select_genome=new s.View({css:"upload-genome",onchange:function(t){i.model.set("genome",t)},data:i.app.list_genomes,container:this.$("#genome"),value:o}),this.model.set("genome",o);var r=this.app.select_extension.value();this.select_extension=new s.View({css:"upload-extension",onchange:function(t){i.model.set("extension",t)},data:i.app.list_extensions,container:this.$("#extension"),value:r}),this.model.set("extension",r),this.$("#symbol").on("click",function(){i._removeRow()}),this.$("#extension-info").on("click",function(t){i.app.showExtensionInfo({$el:a(t.target),title:i.select_extension.text(),extension:i.select_extension.value()})}).on("mousedown",function(t){t.preventDefault()}),this.$("#settings").on("click",function(t){i._showSettings()}).on("mousedown",function(t){t.preventDefault()}),this.$("#text-content").on("change input",function(t){i.model.set("url_paste",a(t.target).val()),i.model.set("file_size",a(t.target).val().length)}),this.model.on("change:percentage",function(){i._refreshPercentage()}),this.model.on("change:status",function(){i._refreshStatus()}),this.model.on("change:info",function(){i._refreshInfo()}),this.model.on("change:genome",function(){i._refreshGenome()}),this.model.on("change:extension",function(){i._refreshExtension()}),this.model.on("change:file_size",function(){i._refreshFileSize()}),this.model.on("remove",function(){i.remove()}),this.app.collection.on("reset",function(){i.remove()})},render:function(){var e=this.model.get("file_name"),i=this.model.get("file_size"),n=this.model.get("file_mode");this.$("#title").html(e),this.$("#size").html(t.bytesToString(i)),this.$("#mode").removeClass().addClass("upload-mode").addClass("text-primary"),"new"==n&&(this.$("#text").css({width:this.$el.width()-16+"px",top:this.$el.height()-8+"px"}).show(),this.$el.height(this.$el.height()-8+this.$("#text").height()+16),this.$("#mode").addClass("fa fa-edit")),"local"==n&&this.$("#mode").addClass("fa fa-laptop"),"ftp"==n&&this.$("#mode").addClass("fa fa-folder-open-o")},remove:function(){this.select_genome.remove(),this.select_extension.remove(),o.View.prototype.remove.apply(this)},_refreshExtension:function(){this.select_extension.value(this.model.get("extension"))},_refreshGenome:function(){this.select_genome.value(this.model.get("genome"))},_refreshInfo:function(){var t=this.model.get("info");t?this.$("#info").html("Failed: "+t).show():this.$("#info").hide()},_refreshPercentage:function(){var t=parseInt(this.model.get("percentage"));this.$(".progress-bar").css({width:t+"%"}),100!=t?this.$("#percentage").html(t+"%"):this.$("#percentage").html("Adding to history...")},_refreshStatus:function(){var t=this.model.get("status");this.$("#symbol").removeClass().addClass(this.status_classes[t]),this.model.set("enabled","init"==t);var e=this.model.get("enabled");this.$("#text-content").attr("disabled",!e),e?(this.select_genome.enable(),this.select_extension.enable()):(this.select_genome.disable(),this.select_extension.disable()),"success"==t&&(this.$el.addClass("success"),this.$("#percentage").html("100%")),"error"==t&&(this.$el.addClass("danger"),this.$(".progress").remove())},_refreshFileSize:function(){var e=this.model.get("file_size");this.$("#size").html(t.bytesToString(e))},_removeRow:function(){var t=this.model.get("status");"init"!=t&&"success"!=t&&"error"!=t||this.app.collection.remove(this.model)},_showSettings:function(){this.settings.visible?this.settings.hide():(this.settings.empty(),this.settings.append(new i(this).$el),this.settings.show())},_template:function(t){return'
You can tell Galaxy to download data from web by entering URL in this box (one per line). You can also directly paste the contents of a file.
 
0%
';
-}})}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(2),i(1))},function(t,e,i){var n,s;(function(o,a,r){n=[i(4),i(53),i(114),i(80),i(26),i(27),i(7),i(88)],s=function(t,e,i,n,s,l,c){return o.View.extend({select_extension:null,select_genome:null,uploadbox:null,upload_size:0,collection:new e.Collection,ftp:null,counter:{announce:0,success:0,error:0,running:0,reset:function(){this.announce=this.success=this.error=this.running=0}},initialize:function(t){this.app=t,this.options=t.options,this.list_extensions=t.list_extensions,this.list_genomes=t.list_genomes,this.ui_button=t.ui_button,this.ftp_upload_site=t.currentFtp();var e=this;this.setElement(this._template()),this.btnLocal=new c.Button({id:"btn-local",title:"Choose local file",onclick:function(){e.uploadbox.select()},icon:"fa fa-laptop"}),this.btnFtp=new c.Button({id:"btn-ftp",title:"Choose FTP file",onclick:function(){e._eventFtp()},icon:"fa fa-folder-open-o"}),this.btnCreate=new c.Button({id:"btn-new",title:"Paste/Fetch data",onclick:function(){e._eventCreate()},icon:"fa fa-edit"}),this.btnStart=new c.Button({id:"btn-start",title:"Start",onclick:function(){e._eventStart()}}),this.btnStop=new c.Button({id:"btn-stop",title:"Pause",onclick:function(){e._eventStop()}}),this.btnReset=new c.Button({id:"btn-reset",title:"Reset",onclick:function(){e._eventReset()}}),this.btnClose=new c.Button({id:"btn-close",title:"Close",onclick:function(){e.app.modal.hide()}});var i=[this.btnLocal,this.btnFtp,this.btnCreate,this.btnStop,this.btnReset,this.btnStart,this.btnClose];for(var n in i)this.$("#upload-buttons").prepend(i[n].$el);var e=this;this.uploadbox=this.$("#upload-box").uploadbox({url:this.app.options.nginx_upload_path,announce:function(t,i){e._eventAnnounce(t,i)},initialize:function(t){return e.app.toData([e.collection.get(t)],e.history_id)},progress:function(t,i){e._eventProgress(t,i)},success:function(t,i){e._eventSuccess(t,i)},error:function(t,i){e._eventError(t,i)},complete:function(){e._eventComplete()},ondragover:function(){e.$(".upload-box").addClass("highlight")},ondragleave:function(){e.$(".upload-box").removeClass("highlight")}}),this.ftp=new s.View({title:"FTP files",container:this.btnFtp.$el}),this.select_extension=new l.View({css:"footer-selection",container:this.$("#footer-extension"),data:a.filter(this.list_extensions,function(t){return!t.composite_files}),value:this.options.default_extension,onchange:function(t){e.updateExtension(t)}}),e.$("#footer-extension-info").on("click",function(t){e.showExtensionInfo({$el:r(t.target),title:e.select_extension.text(),extension:e.select_extension.value(),placement:"top"})}).on("mousedown",function(t){t.preventDefault()}),this.select_genome=new l.View({css:"footer-selection",container:this.$("#footer-genome"),data:this.list_genomes,value:this.options.default_genome,onchange:function(t){e.updateGenome(t)}}),this.collection.on("remove",function(t){e._eventRemove(t)}),this._updateScreen()},_eventAnnounce:function(t,n){this.counter.announce++;var s=new e.Model({id:t,file_name:n.name,file_size:n.size,file_mode:n.mode||"local",file_path:n.path,file_data:n});this.collection.add(s);var o=new i(this,{model:s});this.$("#upload-table > tbody:first").append(o.$el),this._updateScreen(),o.render()},_eventProgress:function(t,e){var i=this.collection.get(t);i.set("percentage",e),this.ui_button.set("percentage",this._uploadPercentage(e,i.get("file_size")))},_eventSuccess:function(t,e){var i=this.collection.get(t);i.set("percentage",100),i.set("status","success"),this.ui_button.set("percentage",this._uploadPercentage(100,i.get("file_size"))),this.upload_completed+=100*i.get("file_size"),this.counter.announce--,this.counter.success++,this._updateScreen(),Galaxy.currHistoryPanel.refreshContents()},_eventError:function(t,e){var i=this.collection.get(t);i.set("percentage",100),i.set("status","error"),i.set("info",e),this.ui_button.set("percentage",this._uploadPercentage(100,i.get("file_size"))),this.ui_button.set("status","danger"),this.upload_completed+=100*i.get("file_size"),this.counter.announce--,this.counter.error++,this._updateScreen()},_eventComplete:function(){this.collection.each(function(t){"queued"==t.get("status")&&t.set("status","init")}),this.counter.running=0,this._updateScreen()},_eventRemove:function(t){var e=t.get("status");"success"==e?this.counter.success--:"error"==e?this.counter.error--:this.counter.announce--,this.uploadbox.remove(t.id),this._updateScreen()},showExtensionInfo:function(t){var e=this,i=t.$el,n=t.extension,o=t.title,r=a.findWhere(e.list_extensions,{id:n});this.extension_popup&&this.extension_popup.remove(),this.extension_popup=new s.View({placement:t.placement||"bottom",container:i}),this.extension_popup.title(o),this.extension_popup.empty(),this.extension_popup.append(this._templateDescription(r)),this.extension_popup.show()},_eventFtp:function(){if(this.ftp.visible)this.ftp.hide();else{this.ftp.empty();var t=this;this.ftp.append(new n({collection:this.collection,ftp_upload_site:this.ftp_upload_site,onadd:function(e){t.uploadbox.add([{mode:"ftp",name:e.path,size:e.size,path:e.path}])},onremove:function(e){t.collection.remove(e)}}).$el),this.ftp.show()}},_eventCreate:function(){this.uploadbox.add([{name:"New File",size:0,mode:"new"}])},_eventStart:function(){if(!(0==this.counter.announce||this.counter.running>0)){var t=this;this.upload_size=0,this.upload_completed=0,this.collection.each(function(e){"init"==e.get("status")&&(e.set("status","queued"),t.upload_size+=e.get("file_size"))}),this.ui_button.set("percentage",0),this.ui_button.set("status","success"),this.counter.running=this.counter.announce,this.history_id=this.app.currentHistory(),this.uploadbox.start(),this._updateScreen()}},_eventStop:function(){this.counter.running>0&&(this.ui_button.set("status","info"),r("#upload-info").html("Queue will pause after completing the current file..."),this.uploadbox.stop())},_eventReset:function(){0==this.counter.running&&(this.collection.reset(),this.counter.reset(),this.uploadbox.reset(),this.select_extension.value(this.options.default_extension),this.select_genome.value(this.options.default_genome),this.ui_button.set("percentage",0),this._updateScreen())},updateExtension:function(t,e){var i=this;this.collection.each(function(n){"init"!=n.get("status")||n.get("extension")!=i.options.default_extension&&e||n.set("extension",t)})},updateGenome:function(t,e){var i=this;this.collection.each(function(n){"init"!=n.get("status")||n.get("genome")!=i.options.default_genome&&e||n.set("genome",t)})},_updateScreen:function(){0==this.counter.announce?this.uploadbox.compatible()?message=" ":message="Browser does not support Drag & Drop. Try Firefox 4+, Chrome 7+, IE 10+, Opera 12+ or Safari 6+.":0==this.counter.running?message="You added "+this.counter.announce+" file(s) to the queue. Add more files or click 'Start' to proceed.":message="Please wait..."+this.counter.announce+" out of "+this.counter.running+" remaining.",this.$("#upload-info").html(message),0==this.counter.running&&this.counter.announce+this.counter.success+this.counter.error>0?this.btnReset.enable():this.btnReset.disable(),0==this.counter.running&&this.counter.announce>0?(this.btnStart.enable(),this.btnStart.$el.addClass("btn-primary")):(this.btnStart.disable(),this.btnStart.$el.removeClass("btn-primary")),this.counter.running>0?this.btnStop.enable():this.btnStop.disable(),0==this.counter.running?(this.btnLocal.enable(),this.btnFtp.enable(),this.btnCreate.enable()):(this.btnLocal.disable(),this.btnFtp.disable(),this.btnCreate.disable()),this.ftp_upload_site?this.btnFtp.$el.show():this.btnFtp.$el.hide(),this.counter.announce+this.counter.success+this.counter.error>0?(this.$("#upload-table").show(),this.$(".upload-helper").hide()):(this.$("#upload-table").hide(),this.$(".upload-helper").show())},_uploadPercentage:function(t,e){return(this.upload_completed+t*e)/this.upload_size},_templateDescription:function(t){if(t.description){var e=t.description;return t.description_url&&(e+=' (read more)'),e}return"There is no description available for this file extension."},_template:function(){return'
Drop files here
Name
Size
Type
Genome
Settings
Status
'}})}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(2),i(3),i(1))},function(t,e,i){var n,s;(function(i,o){n=[],s=function(){var t=i.Model.extend({defaults:{icon:"fa-upload",tooltip:"Download from URL or upload files from disk",label:"Load Data",percentage:0,status:""}}),e=i.View.extend({model:null,initialize:function(t){var e=this,i=t.model;this.setElement(this._template()),this.$el.on("click",function(e){t.onclick(e)}),this.$el.tooltip({title:i.get("tooltip"),placement:"bottom"}),i.on("change:percentage",function(){e._percentage(i.get("percentage"))}),i.on("change:status",function(){e._status(i.get("status"))});var e=this;o(window).on("beforeunload",function(){var e="";return t.onunload&&(e=t.onunload()),""!=e?e:void 0})},_status:function(t){var e=this.$el.find(".progress-bar");e.removeClass(),e.addClass("progress-bar"),e.addClass("progress-bar-notransition"),""!=t&&e.addClass("progress-bar-"+t)},_percentage:function(t){var e=this.$el.find(".progress-bar");e.css({width:t+"%"})},_template:function(){return'
'}});return{Model:t,View:e}}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(2),i(1))},function(t,e,i){var n,s;(function(o){n=[i(4),i(8),i(111),i(116),i(115),i(113)],s=function(t,e,i,n,s,a){return o.View.extend({options:{nginx_upload_path:"",ftp_upload_site:"n/a",default_genome:"?",default_extension:"auto",height:500,width:900,auto:{id:"auto",text:"Auto-detect",description:"This system will try to detect the file type automatically. If your file is not detected properly as one of the known formats, it most likely means that it has some format problems (e.g., different number of columns on different rows). You can still coerce the system to set your data to the format you think it should be. You can also upload compressed files, which will automatically be decompressed."}},modal:null,ui_button:null,current_history:null,list_extensions:[],list_genomes:[],initialize:function(e){var i=this;this.options=t.merge(e,this.options),this.ui_button=new n.Model,this.ui_button_view=new n.View({model:this.ui_button,onclick:function(t){t.preventDefault(),i.show()},onunload:function(){var t=i.ui_button.get("percentage",0);return t>0&&100>t?"Several uploads are queued.":void 0}}),this.setElement(this.ui_button_view.$el);var i=this;t.get({url:Galaxy.root+"api/datatypes?extension_only=False",success:function(t){for(key in t)i.list_extensions.push({id:t[key].extension,text:t[key].extension,description:t[key].description,description_url:t[key].description_url,composite_files:t[key].composite_files});i.list_extensions.sort(function(t,e){var i=t.text&&t.text.toLowerCase(),n=e.text&&e.text.toLowerCase();return i>n?1:n>i?-1:0}),i.options.datatypes_disable_auto||i.list_extensions.unshift(i.options.auto)}}),t.get({url:Galaxy.root+"api/genomes",success:function(t){for(key in t)i.list_genomes.push({id:t[key][1],text:t[key][0]});i.list_genomes.sort(function(t,e){return t.id==i.options.default_genome?-1:e.id==i.options.default_genome?1:t.text>e.text?1:t.text0){var n={};n.dbkey=t[0].get("genome",null),n.file_type=t[0].get("extension",null);for(var s in t){var o=t[s];if(o.set("status","running"),!(o.get("file_size")>0)){i.error_message="Upload content incomplete.",o.set("status","error"),o.set("info",i.error_message);break}var a="files_"+s+"|";switch(n[a+"type"]="upload_dataset",n[a+"space_to_tab"]=o.get("space_to_tab")&&"Yes"||null,n[a+"to_posix_lines"]=o.get("to_posix_lines")&&"Yes"||null,o.get("file_mode")){case"new":n[a+"url_paste"]=o.get("url_paste");break;case"ftp":n[a+"ftp_files"]=o.get("file_path");break;case"local":i.files.push({name:a+"file_data",file:o.get("file_data")})}}i.payload.inputs=JSON.stringify(n)}return i}})}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(2))},,,,function(t,e,i){var n,s,o;(function(i,a){!function(i,a){s=[],n=a,o="function"==typeof n?n.apply(e,s):n,!(void 0!==o&&(t.exports=o))}(this,function(){function t(t,e){function n(){var i=['
','
','','loading...',"
","
"].join("\n"),n=a(i).hide().css(e.css||{position:"fixed"}),s=n.children(".loading-indicator-text");return e.cover?(n.css({"z-index":2,top:t.css("top"),bottom:t.css("bottom"),left:t.css("left"),right:t.css("right"),opacity:.5,"background-color":"white","text-align":"center"}),s=n.children(".loading-indicator-text").css({"margin-top":"20px"})):(s=n.children(".loading-indicator-text").css({margin:"12px 0px 0px 10px",opacity:"0.85",color:"grey"}),s.children(".loading-indicator-message").css({margin:"0px 8px 0px 0px","font-style":"italic"})),n}var s=this;return e=i.extend({cover:!1},e||{}),s.show=function(e,i,o){return e=e||"loading...",i=i||"fast",t.parent().find(".loading-indicator").remove(),s.$indicator=n().insertBefore(t),s.message(e),s.$indicator.fadeIn(i,o),s},s.message=function(t){s.$indicator.find("i").text(t)},s.hide=function(t,e){return t=t||"fast",s.$indicator&&s.$indicator.size()?s.$indicator.fadeOut(t,function(){s.$indicator.remove(),e&&e()}):e&&e(),s},s}return t})}).call(e,i(1),i(1))},,,function(t,e,i){var n,s;n=[],s=function(){function t(t,e){if(0===t.length)return e.length;if(0===e.length)return t.length;var i,n=[];for(i=0;i<=e.length;i++)n[i]=[i];var s;for(s=0;s<=t.length;s++)n[0][s]=s;for(i=1;i<=e.length;i++)for(s=1;s<=t.length;s++)e.charAt(i-1)===t.charAt(s-1)?n[i][s]=n[i-1][s-1]:n[i][s]=Math.min(n[i-1][s-1]+1,Math.min(n[i][s-1]+1,n[i-1][s]+1));return n[e.length][t.length]}return t}.apply(e,n),!(void 0!==s&&(t.exports=s))}]);
+webpackJsonp([0,1],[
+/* 0 */
+/*!*****************************************!*\
+ !*** ./galaxy/scripts/apps/analysis.js ***!
+ \*****************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ /* WEBPACK VAR INJECTION */(function(_, Backbone) {
+ var jQuery = __webpack_require__( /*! jquery */ 3 ),
+ $ = jQuery,
+ GalaxyApp = __webpack_require__( /*! galaxy */ 4 ).GalaxyApp,
+ QUERY_STRING = __webpack_require__( /*! utils/query-string-parsing */ 11 ),
+ PANEL = __webpack_require__( /*! layout/panel */ 12 ),
+ ToolPanel = __webpack_require__( /*! ./tool-panel */ 13 ),
+ HistoryPanel = __webpack_require__( /*! ./history-panel */ 60 ),
+ PAGE = __webpack_require__( /*! layout/page */ 101 ),
+ ToolForm = __webpack_require__( /*! mvc/tool/tool-form */ 20 ),
+ Tours = __webpack_require__( /*! mvc/tours */ 104 );
+
+ /** define the 'Analyze Data'/analysis/main/home page for Galaxy
+ * * has a masthead
+ * * a left tool menu to allow the user to load tools in the center panel
+ * * a right history menu that shows the user's current data
+ * * a center panel
+ * Both panels (generally) persist while the center panel shows any
+ * UI needed for the current step of an analysis, like:
+ * * tool forms to set tool parameters,
+ * * tables showing the contents of datasets
+ * * etc.
+ */
+ window.app = function app( options, bootstrapped ){
+ window.Galaxy = new GalaxyApp( options, bootstrapped );
+ Galaxy.debug( 'analysis app' );
+ // TODO: use router as App base (combining with Galaxy)
+
+ // .................................................... panels and page
+ var config = options.config,
+ toolPanel = new ToolPanel({
+ el : '#left',
+ userIsAnonymous : Galaxy.user.isAnonymous(),
+ search_url : config.search_url,
+ toolbox : config.toolbox,
+ toolbox_in_panel : config.toolbox_in_panel,
+ stored_workflow_menu_entries : config.stored_workflow_menu_entries,
+ nginx_upload_path : config.nginx_upload_path,
+ ftp_upload_site : config.ftp_upload_site,
+ default_genome : config.default_genome,
+ default_extension : config.default_extension,
+ }),
+ centerPanel = new PANEL.CenterPanel({
+ el : '#center'
+ }),
+ historyPanel = new HistoryPanel({
+ el : '#right',
+ galaxyRoot : Galaxy.root,
+ userIsAnonymous : Galaxy.user.isAnonymous(),
+ allow_user_dataset_purge: config.allow_user_dataset_purge,
+ }),
+ analysisPage = new PAGE.PageLayoutView( _.extend( options, {
+ el : 'body',
+ left : toolPanel,
+ center : centerPanel,
+ right : historyPanel,
+ }));
+
+ // .................................................... decorate the galaxy object
+ // TODO: most of this is becoming unnecessary as we move to apps
+ Galaxy.page = analysisPage;
+ Galaxy.params = Galaxy.config.params;
+
+ // add tool panel to Galaxy object
+ Galaxy.toolPanel = toolPanel.tool_panel;
+ Galaxy.upload = toolPanel.uploadButton;
+
+ Galaxy.currHistoryPanel = historyPanel.historyView;
+ Galaxy.currHistoryPanel.listenToGalaxy( Galaxy );
+
+ //HACK: move there
+ Galaxy.app = {
+ display : function( view, target ){
+ // TODO: Remove this line after select2 update
+ $( '.select2-hidden-accessible' ).remove();
+ centerPanel.display( view );
+ },
+ };
+
+ // .................................................... routes
+ /** */
+ var router = new ( Backbone.Router.extend({
+ // TODO: not many client routes at this point - fill and remove from server.
+ // since we're at root here, this may be the last to be routed entirely on the client.
+ initialize : function( options ){
+ this.options = options;
+ },
+
+ /** override to parse query string into obj and send to each route */
+ execute: function( callback, args, name ){
+ Galaxy.debug( 'router execute:', callback, args, name );
+ var queryObj = QUERY_STRING.parse( args.pop() );
+ args.push( queryObj );
+ if( callback ){
+ callback.apply( this, args );
+ }
+ },
+
+ routes : {
+ '(/)' : 'home',
+ // TODO: remove annoying 'root' from root urls
+ '(/)root*' : 'home',
+ '(/)tours(/)(:tour_id)' : 'show_tours',
+ },
+
+ show_tours : function( tour_id ){
+ if (tour_id){
+ Tours.giveTour(tour_id);
+ }
+ else{
+ centerPanel.display( new Tours.ToursView() );
+ }
+ },
+
+ /** */
+ home : function( params ){
+ // TODO: to router, remove Globals
+ // load a tool by id (tool_id) or rerun a previous tool execution (job_id)
+ if( params.tool_id || params.job_id ) {
+ if ( params.tool_id === 'upload1' ) {
+ Galaxy.upload.show();
+ this._loadCenterIframe( 'welcome' );
+ } else {
+ this._loadToolForm( params );
+ }
+ } else {
+ // show the workflow run form
+ if( params.workflow_id ){
+ this._loadCenterIframe( 'workflow/run?id=' + params.workflow_id );
+ // load the center iframe with controller.action: galaxy.org/?m_c=history&m_a=list -> history/list
+ } else if( params.m_c ){
+ this._loadCenterIframe( params.m_c + '/' + params.m_a );
+ // show the workflow run form
+ } else {
+ this._loadCenterIframe( 'welcome' );
+ }
+ }
+ },
+
+ /** load the center panel with a tool form described by the given params obj */
+ _loadToolForm : function( params ){
+ //TODO: load tool form code async
+ params.id = params.tool_id;
+ centerPanel.display( new ToolForm.View( params ) );
+ },
+
+ /** load the center panel iframe using the given url */
+ _loadCenterIframe : function( url, root ){
+ root = root || Galaxy.root;
+ url = root + url;
+ centerPanel.$( '#galaxy_main' ).prop( 'src', url );
+ },
+
+ }))( options );
+
+ // .................................................... when the page is ready
+ // render and start the router
+ $(function(){
+ analysisPage
+ .render()
+ .right.historyView.loadCurrentHistory();
+
+ // use galaxy to listen to history size changes and then re-fetch the user's total size (to update the quota meter)
+ // TODO: we have to do this here (and after every page.render()) because the masthead is re-created on each
+ // page render. It's re-created each time because there is no render function and can't be re-rendered without
+ // re-creating it.
+ Galaxy.listenTo( analysisPage.right.historyView, 'history-size-change', function(){
+ // fetch to update the quota meter adding 'current' for any anon-user's id
+ Galaxy.user.fetch({ url: Galaxy.user.urlRoot() + '/' + ( Galaxy.user.id || 'current' ) });
+ });
+ analysisPage.right.historyView.connectToQuotaMeter( analysisPage.masthead.quotaMeter );
+
+ // start the router - which will call any of the routes above
+ Backbone.history.start({
+ root : Galaxy.root,
+ pushState : true,
+ });
+ });
+ };
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! underscore */ 1), __webpack_require__(/*! libs/backbone */ 2)))
+
+/***/ },
+/* 1 */,
+/* 2 */,
+/* 3 */,
+/* 4 */,
+/* 5 */,
+/* 6 */,
+/* 7 */,
+/* 8 */,
+/* 9 */,
+/* 10 */,
+/* 11 */,
+/* 12 */,
+/* 13 */
+/*!*******************************************!*\
+ !*** ./galaxy/scripts/apps/tool-panel.js ***!
+ \*******************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ /* WEBPACK VAR INJECTION */(function($, _) {var LeftPanel = __webpack_require__( /*! layout/panel */ 12 ).LeftPanel,
+ Tools = __webpack_require__( /*! mvc/tool/tools */ 14 ),
+ Upload = __webpack_require__( /*! mvc/upload/upload-view */ 47 ),
+ _l = __webpack_require__( /*! utils/localization */ 7 );
+
+ /* Builds the tool menu panel on the left of the analysis page */
+ var ToolPanel = LeftPanel.extend({
+
+ title : _l( 'Tools' ),
+
+ initialize: function( options ){
+ LeftPanel.prototype.initialize.call( this, options );
+ this.log( this + '.initialize:', options );
+
+ /** @type {Object[]} descriptions of user's workflows to be shown in the tool menu */
+ this.stored_workflow_menu_entries = options.stored_workflow_menu_entries || [];
+
+ // create tool search, tool panel, and tool panel view.
+ var tool_search = new Tools.ToolSearch({
+ search_url : options.search_url,
+ hidden : false
+ });
+ var tools = new Tools.ToolCollection( options.toolbox );
+ this.tool_panel = new Tools.ToolPanel({
+ tool_search : tool_search,
+ tools : tools,
+ layout : options.toolbox_in_panel
+ });
+ this.tool_panel_view = new Tools.ToolPanelView({ model: this.tool_panel });
+
+ // add upload modal
+ this.uploadButton = new Upload({
+ nginx_upload_path : options.nginx_upload_path,
+ ftp_upload_site : options.ftp_upload_site,
+ default_genome : options.default_genome,
+ default_extension : options.default_extension,
+ });
+ },
+
+ render : function(){
+ var self = this;
+ LeftPanel.prototype.render.call( self );
+ self.$( '.panel-header-buttons' ).append( self.uploadButton.$el );
+
+ // if there are tools, render panel and display everything
+ if (self.tool_panel.get( 'layout' ).size() > 0) {
+ self.tool_panel_view.render();
+ //TODO: why the hide/show?
+ self.$( '.toolMenu' ).show();
+ }
+ self.$( '.toolMenuContainer' ).prepend( self.tool_panel_view.$el );
+
+ self._renderWorkflowMenu();
+
+ // if a tool link has the minsizehint attribute, handle it here (gen. by hiding the tool panel)
+ self.$( 'a[minsizehint]' ).click( function() {
+ if ( parent.handle_minwidth_hint ) {
+ parent.handle_minwidth_hint( $( self ).attr( 'minsizehint' ) );
+ }
+ });
+ },
+
+ /** build the dom for the workflow portion of the tool menu */
+ _renderWorkflowMenu : function(){
+ var self = this;
+ // add internal workflow list
+ self.$( '#internal-workflows' ).append( self._templateTool({
+ title : _l( 'All workflows' ),
+ href : 'workflow/list_for_run'
+ }));
+ _.each( self.stored_workflow_menu_entries, function( menu_entry ){
+ self.$( '#internal-workflows' ).append( self._templateTool({
+ title : menu_entry.stored_workflow.name,
+ href : 'workflow/run?id=' + menu_entry.encoded_stored_workflow_id
+ }));
+ });
+ },
+
+ /** build a link to one tool */
+ _templateTool: function( tool ) {
+ return [
+ '
'
+ ].join('')),
+
+ // a single tool's link in the tool panel; will load the tool form in the center panel
+ tool_link : _.template([
+ '',
+ '<% _.each( labels, function( label ){ %>',
+ '',
+ '<%- label %>',
+ '',
+ '<% }); %>',
+ '',
+ '',
+ '<%- name %>',
+ '',
+ ' <%- description %>'
+ ].join('')),
+
+ // the tool form for entering tool parameters, viewing help and executing the tool
+ // loaded when a tool link is clicked in the tool panel
+ tool_form : _.template([
+ '
');
+ }
+ }
+
+ // Render first chunk.
+ var self = this,
+ first_chunk = this.model.get('first_data_chunk');
+ if (first_chunk) {
+ // First chunk is bootstrapped, so render now.
+ this._renderChunk(first_chunk);
+ }
+ else {
+ // No bootstrapping, so get first chunk and then render.
+ $.when(self.model.get_next_chunk()).then(function(result) {
+ self._renderChunk(result);
+ });
+ }
+
+ // -- Show new chunks during scrolling. --
+
+ // Set up chunk loading when scrolling using the scrolling element.
+ this.scroll_elt.scroll(function(){
+ self.attempt_to_fetch();
+ });
+ },
+
+ /**
+ * Returns true if user has scrolled to the bottom of the view.
+ */
+ scrolled_to_bottom: function() {
+ return false;
+ },
+
+ // -- Helper functions. --
+
+ _renderCell: function(cell_contents, index, colspan) {
+ var $cell = $('
').text(cell_contents);
+ var column_types = this.model.get_metadata('column_types');
+ if (colspan !== undefined) {
+ $cell.attr('colspan', colspan).addClass('stringalign');
+ } else if (column_types) {
+ if (index < column_types.length) {
+ if (column_types[index] === 'str' || column_types[index] === 'list') {
+ /* Left align all str columns, right align the rest */
+ $cell.addClass('stringalign');
+ }
+ }
+ }
+ return $cell;
+ },
+
+ _renderRow: function(line) {
+ // Check length of cells to ensure this is a complete row.
+ var cells = line.split('\t'),
+ row = $('
'),
+ num_columns = this.model.get_metadata('columns');
+
+ if (this.row_count % 2 !== 0) {
+ row.addClass('dark_row');
+ }
+
+ if (cells.length === num_columns) {
+ _.each(cells, function(cell_contents, index) {
+ row.append(this._renderCell(cell_contents, index));
+ }, this);
+ }
+ else if (cells.length > num_columns) {
+ // SAM file or like format with optional metadata included.
+ _.each(cells.slice(0, num_columns - 1), function(cell_contents, index) {
+ row.append(this._renderCell(cell_contents, index));
+ }, this);
+ row.append(this._renderCell(cells.slice(num_columns - 1).join('\t'), num_columns - 1));
+ }
+ else if (num_columns > 5 && cells.length === num_columns - 1 ) {
+ // SAM file or like format with optional metadata missing.
+ _.each(cells, function(cell_contents, index) {
+ row.append(this._renderCell(cell_contents, index));
+ }, this);
+ row.append($('
'));
+ }
+ else {
+ // Comment line, just return the one cell.
+ row.append(this._renderCell(line, 0, num_columns));
+ }
+
+ this.row_count++;
+ return row;
+ },
+
+ _renderChunk: function(chunk) {
+ var data_table = this.$el.find('table');
+ _.each(chunk.ck_data.split('\n'), function(line, index) {
+ if (line !== ''){
+ data_table.append(this._renderRow(line));
+ }
+ }, this);
+ }
+ });
+
+ /**
+ * Tabular view that is placed at the top level of page. Scrolling occurs
+ * view top-level elements outside of view.
+ */
+ var TopLevelTabularDatasetChunkedView = TabularDatasetChunkedView.extend({
+
+ initialize: function(options) {
+ TabularDatasetChunkedView.prototype.initialize.call(this, options);
+
+ // Scrolling happens in top-level elements.
+ scroll_elt = _.find(this.$el.parents(), function(p) {
+ return $(p).css('overflow') === 'auto';
+ });
+
+ // If no scrolling element found, use window.
+ if (!scroll_elt) { scroll_elt = window; }
+
+ // Wrap scrolling element for easy access.
+ this.scroll_elt = $(scroll_elt);
+ },
+
+ /**
+ * Returns true if user has scrolled to the bottom of the view.
+ */
+ scrolled_to_bottom: function() {
+ return (this.$el.height() - this.scroll_elt.scrollTop() - this.scroll_elt.height() <= 0);
+ }
+
+ });
+
+ /**
+ * Tabular view tnat is embedded in a page. Scrolling occurs in view's el.
+ */
+ var EmbeddedTabularDatasetChunkedView = TabularDatasetChunkedView.extend({
+
+ initialize: function(options) {
+ TabularDatasetChunkedView.prototype.initialize.call(this, options);
+
+ // Because view is embedded, set up div to do scrolling.
+ this.scroll_elt = this.$el.css({
+ position: 'relative',
+ overflow: 'scroll',
+ height: options.height || '500px'
+ });
+ },
+
+ /**
+ * Returns true if user has scrolled to the bottom of the view.
+ */
+ scrolled_to_bottom: function() {
+ return this.$el.scrollTop() + this.$el.innerHeight() >= this.el.scrollHeight;
+ }
+
+ });
+
+ // button for trackster visualization
+ var TabularButtonTracksterView = Backbone.View.extend({
+
+ // gene region columns
+ col: {
+ chrom : null,
+ start : null,
+ end : null
+ },
+
+ // url for trackster
+ url_viz : null,
+
+ // dataset id
+ dataset_id : null,
+
+ // database key
+ genome_build: null,
+
+ // data type
+ file_ext : null,
+
+ // backbone initialize
+ initialize: function (options) {
+ // check if environment is available
+ var Galaxy = parent.Galaxy;
+
+ // link galaxy modal or create one
+ if (Galaxy && Galaxy.modal) {
+ this.modal = Galaxy.modal;
+ }
+
+ // link galaxy frames
+ if (Galaxy && Galaxy.frame) {
+ this.frame = Galaxy.frame;
+ }
+
+ // check
+ if (!this.modal || !this.frame) {
+ return;
+ }
+
+ // model/metadata
+ var model = options.model;
+ var metadata = model.get('metadata');
+
+ // check for datatype
+ if (!model.get('file_ext')) {
+ return;
+ }
+
+ // get data type
+ this.file_ext = model.get('file_ext');
+
+ // check for bed-file format
+ if (this.file_ext == 'bed')
+ {
+ // verify that metadata exists
+ if (metadata.get('chromCol') && metadata.get('startCol') && metadata.get('endCol'))
+ {
+ // read in columns
+ this.col.chrom = metadata.get('chromCol') - 1;
+ this.col.start = metadata.get('startCol') - 1;
+ this.col.end = metadata.get('endCol') - 1;
+ } else {
+ console.log('TabularButtonTrackster : Bed-file metadata incomplete.');
+ return;
+ }
+ }
+
+ // check for vcf-file format
+ if (this.file_ext == 'vcf')
+ {
+ // search array
+ function search (str, array) {
+ for (var j = 0; j < array.length; j++)
+ if (array[j].match(str)) return j;
+ return -1;
+ };
+
+ // load
+ this.col.chrom = search('Chrom', metadata.get('column_names'));
+ this.col.start = search('Pos', metadata.get('column_names'));
+ this.col.end = null;
+
+ // verify that metadata exists
+ if (this.col.chrom == -1 || this.col.start == -1) {
+ console.log('TabularButtonTrackster : VCF-file metadata incomplete.');
+ return;
+ }
+ }
+
+ // check
+ if(this.col.chrom === undefined) {
+ return;
+ }
+
+ // get dataset id
+ if (model.id) {
+ this.dataset_id = model.id;
+ } else {
+ console.log('TabularButtonTrackster : Dataset identification is missing.');
+ return;
+ }
+
+ // get url
+ if (model.get('url_viz')) {
+ this.url_viz = model.get('url_viz');
+ } else {
+ console.log('TabularButtonTrackster : Url for visualization controller is missing.');
+ return;
+ }
+
+ // get genome_build / database key
+ if (model.get('genome_build')) {
+ this.genome_build = model.get('genome_build');
+ }
+
+ // create the icon
+ var btn_viz = new mod_icon_btn.IconButtonView({
+ model : new mod_icon_btn.IconButton({
+ title : 'Visualize',
+ icon_class : 'chart_curve',
+ id : 'btn_viz'
+ })
+ });
+
+ // set element
+ this.setElement(options.$el);
+
+ // add to element
+ this.$el.append(btn_viz.render().$el);
+
+ // hide the button
+ this.hide();
+ },
+
+ // backbone events
+ events:
+ {
+ 'mouseover tr' : 'show',
+ 'mouseleave' : 'hide'
+ },
+
+ // show button
+ show: function (e) {
+ // is numeric
+ function is_numeric(n) {
+ return !isNaN(parseFloat(n)) && isFinite(n);
+ };
+
+ // check
+ if(this.col.chrom === null)
+ return;
+
+ // get selected data line
+ var row = $(e.target).parent();
+
+ // verify that location has been found
+ var chrom = row.children().eq(this.col.chrom).html();
+ var start = row.children().eq(this.col.start).html();
+
+ // end is optional
+ var end = this.col.end ? row.children().eq(this.col.end).html() : start;
+
+ // double check location
+ if (!chrom.match("^#") && chrom !== "" && is_numeric(start)) {
+
+ // get target gene region
+ var btn_viz_pars = {
+ dataset_id : this.dataset_id,
+ gene_region : chrom + ":" + start + "-" + end
+ };
+
+ // get button position
+ var offset = row.offset();
+ var left = offset.left - 10;
+ var top = offset.top - $(window).scrollTop() + 3;
+
+ // update css
+ $('#btn_viz').css({'position': 'fixed', 'top': top + 'px', 'left': left + 'px'});
+ $('#btn_viz').off('click');
+ $('#btn_viz').click(this.create_trackster_action(this.url_viz, btn_viz_pars, this.genome_build));
+
+ // show the button
+ $('#btn_viz').show();
+ } else {
+ // hide the button
+ $('#btn_viz').hide();
+ }
+ },
+
+ // hide button
+ hide: function () {
+ this.$el.find('#btn_viz').hide();
+ },
+
+ // create action
+ create_trackster_action : function (vis_url, dataset_params, dbkey) {
+ // link this
+ var self = this;
+
+ // create function
+ return function() {
+ var listTracksParams = {};
+ if (dbkey) {
+ listTracksParams[ 'f-dbkey' ] = dbkey;
+ }
+ $.ajax({
+ url: vis_url + '/list_tracks?' + $.param( listTracksParams ),
+ dataType: 'html',
+ error: function() {
+ // show error message
+ self.modal.show({
+ title : 'Something went wrong!',
+ body : 'Unfortunately we could not add this dataset to the track browser. Please try again or contact us.',
+ buttons : {
+ 'Cancel': function(){
+ self.modal.hide();
+ }
+ }
+ });
+ },
+ success: function(table_html) {
+ self.modal.show({
+ title : 'View Data in a New or Saved Visualization',
+ buttons :{
+ 'Cancel': function(){
+ self.modal.hide();
+ },
+ 'View in saved visualization': function(){
+ // show modal with saved visualizations
+ self.modal.show(
+ {
+ title : 'Add Data to Saved Visualization',
+ body : table_html,
+ buttons : {
+ 'Cancel': function(){
+ self.modal.hide();
+ },
+ 'Add to visualization': function(){
+ // hide
+ self.modal.hide();
+
+ // search selected fields
+ self.modal.$el.find('input[name=id]:checked').each(function(){
+ // get visualization id
+ var vis_id = $(this).val();
+ dataset_params.id = vis_id;
+
+ // add widget
+ self.frame.add({
+ title : 'Trackster',
+ type : 'url',
+ content : vis_url + '/trackster?' + $.param(dataset_params)
+ });
+ });
+ }
+ }
+ });
+ },
+ 'View in new visualization': function(){
+ // hide
+ self.modal.hide();
+
+ // add widget
+ self.frame.add({
+ title : 'Trackster',
+ type : 'url',
+ content : vis_url + '/trackster?' + $.param(dataset_params)
+ });
+ }
+ }
+ });
+ }
+ });
+ return false;
+ };
+ }
+ });
+
+ // -- Utility functions. --
+
+ /**
+ * Create a model, attach it to a view, render view, and attach it to a parent element.
+ */
+ var createModelAndView = function(model, view, model_config, parent_elt) {
+ // Create model, view.
+ var a_view = new view({
+ model: new model(model_config)
+ });
+
+ // Render view and add to parent element.
+ a_view.render();
+ if (parent_elt) {
+ parent_elt.append(a_view.$el);
+ }
+
+ return a_view;
+ };
+
+ /**
+ * Create a tabular dataset chunked view (and requisite tabular dataset model)
+ * and appends to parent_elt.
+ */
+ var createTabularDatasetChunkedView = function(options) {
+ // If no model, create and set model from dataset config.
+ if (!options.model) {
+ options.model = new TabularDataset(options.dataset_config);
+ }
+
+ var parent_elt = options.parent_elt;
+ var embedded = options.embedded;
+
+ // Clean up options so that only needed options are passed to view.
+ delete options.embedded;
+ delete options.parent_elt;
+ delete options.dataset_config;
+
+ // Create and set up view.
+ var view = (embedded ? new EmbeddedTabularDatasetChunkedView(options) :
+ new TopLevelTabularDatasetChunkedView(options));
+ view.render();
+
+ if (parent_elt) {
+ parent_elt.append(view.$el);
+ // If we're sticking this in another element, once it's appended check
+ // to make sure we've filled enough space.
+ // Without this, the scroll elements don't work.
+ view.expand_to_container();
+ }
+
+ return view;
+ };
+
+ return {
+ Dataset: Dataset,
+ TabularDataset: TabularDataset,
+ DatasetCollection: DatasetCollection,
+ TabularDatasetChunkedView: TabularDatasetChunkedView,
+ createTabularDatasetChunkedView: createTabularDatasetChunkedView
+ };
+
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! libs/backbone */ 2), __webpack_require__(/*! underscore */ 1), __webpack_require__(/*! jquery */ 3)))
+
+/***/ },
+/* 17 */,
+/* 18 */,
+/* 19 */
+/*!**********************************************!*\
+ !*** ./galaxy/scripts/mvc/ui/icon-button.js ***!
+ \**********************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(Backbone, _, $) {!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
+ //jquery
+ //backbone
+ ], __WEBPACK_AMD_DEFINE_RESULT__ = function(){
+ //=============================================================================
+ /**
+ * backbone model for icon buttons
+ */
+ var IconButton = Backbone.Model.extend({
+ defaults: {
+ title : "",
+ icon_class : "",
+ on_click : null,
+ menu_options : null,
+ is_menu_button : true,
+ id : null,
+ href : null,
+ target : null,
+ enabled : true,
+ visible : true,
+ tooltip_config : {}
+ }
+ });
+
+ /**
+ * backbone view for icon buttons
+ */
+ var IconButtonView = Backbone.View.extend({
+
+ initialize : function(){
+ // better rendering this way
+ this.model.attributes.tooltip_config = { placement : 'bottom' };
+ this.model.bind( 'change', this.render, this );
+ },
+
+ render : function( ){
+ // hide tooltip
+ this.$el.tooltip( 'hide' );
+
+ var new_elem = this.template( this.model.toJSON() );
+ // configure tooltip
+ new_elem.tooltip( this.model.get( 'tooltip_config' ));
+ this.$el.replaceWith( new_elem );
+ this.setElement( new_elem );
+ return this;
+ },
+
+ events : {
+ 'click' : 'click'
+ },
+
+ click : function( event ){
+ // if on_click pass to that function
+ if( _.isFunction( this.model.get( 'on_click' ) ) ){
+ this.model.get( 'on_click' )( event );
+ return false;
+ }
+ // otherwise, bubble up ( to href or whatever )
+ return true;
+ },
+
+ // generate html element
+ template: function( options ){
+ var buffer = 'title="' + options.title + '" class="icon-button';
+
+ if( options.is_menu_button ){
+ buffer += ' menu-button';
+ }
+
+ buffer += ' ' + options.icon_class;
+
+ if( !options.enabled ){
+ buffer += '_disabled';
+ }
+
+ // close class tag
+ buffer += '"';
+
+ if( options.id ){
+ buffer += ' id="' + options.id + '"';
+ }
+
+ buffer += ' href="' + options.href + '"';
+ // add target for href
+ if( options.target ){
+ buffer += ' target="' + options.target + '"';
+ }
+ // set visibility
+ if( !options.visible ){
+ buffer += ' style="display: none;"';
+ }
+
+ // enabled/disabled
+ if ( options.enabled ){
+ buffer = '';
+ } else {
+ buffer = '';
+ }
+
+ // return element
+ return $( buffer );
+ }
+ } );
+
+ // define collection
+ var IconButtonCollection = Backbone.Collection.extend({
+ model: IconButton
+ });
+
+ /**
+ * menu with multiple icon buttons
+ * views are not needed nor used for individual buttons
+ */
+ var IconButtonMenuView = Backbone.View.extend({
+
+ tagName: 'div',
+
+ initialize: function(){
+ this.render();
+ },
+
+ render: function(){
+ // initialize icon buttons
+ var self = this;
+ this.collection.each(function(button){
+ // create and add icon button to menu
+ var elt = $('')
+ .attr('href', 'javascript:void(0)')
+ .attr('title', button.attributes.title)
+ .addClass('icon-button menu-button')
+ .addClass(button.attributes.icon_class)
+ .appendTo(self.$el)
+ .click(button.attributes.on_click);
+
+ // configure tooltip
+ if (button.attributes.tooltip_config){
+ elt.tooltip(button.attributes.tooltip_config);
+ }
+
+ // add popup menu to icon
+ var menu_options = button.get('options');
+ if (menu_options){
+ make_popupmenu(elt, menu_options);
+ }
+ });
+
+ // return
+ return this;
+ }
+ });
+
+ /**
+ * Returns an IconButtonMenuView for the provided configuration.
+ * Configuration is a list of dictionaries where each dictionary
+ * defines an icon button. Each dictionary must have the following
+ * elements: icon_class, title, and on_click.
+ */
+ var create_icon_buttons_menu = function(config, global_config)
+ {
+ // initialize global configuration
+ if (!global_config) global_config = {};
+
+ // create and initialize menu
+ var buttons = new IconButtonCollection(
+ _.map(config, function(button_config){
+ return new IconButton(_.extend(button_config, global_config));
+ })
+ );
+
+ // return menu
+ return new IconButtonMenuView( {collection: buttons} );
+ };
+
+
+ //=============================================================================
+ return {
+ IconButton : IconButton,
+ IconButtonView : IconButtonView,
+ IconButtonCollection : IconButtonCollection,
+ IconButtonMenuView : IconButtonMenuView,
+ create_icon_buttons_menu: create_icon_buttons_menu
+ };
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__))
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! libs/backbone */ 2), __webpack_require__(/*! underscore */ 1), __webpack_require__(/*! jquery */ 3)))
+
+/***/ },
+/* 20 */
+/*!**********************************************!*\
+ !*** ./galaxy/scripts/mvc/tool/tool-form.js ***!
+ \**********************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/**
+ This is the regular tool form.
+ */
+ !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! utils/utils */ 21), __webpack_require__(/*! mvc/ui/ui-misc */ 22), __webpack_require__(/*! mvc/tool/tool-form-base */ 28), __webpack_require__(/*! mvc/tool/tool-template */ 43)], __WEBPACK_AMD_DEFINE_RESULT__ = function( Utils, Ui, ToolFormBase, ToolTemplate ) {
+ var View = ToolFormBase.extend({
+ initialize: function( options ) {
+ var self = this;
+ ToolFormBase.prototype.initialize.call( this, Utils.merge({
+ customize : function( options ) {
+ // build execute button
+ options.buttons = {
+ execute : execute_btn = new Ui.Button({
+ icon : 'fa-check',
+ tooltip : 'Execute: ' + options.name + ' (' + options.version + ')',
+ title : 'Execute',
+ cls : 'ui-button btn btn-primary',
+ floating : 'clear',
+ onclick : function() {
+ execute_btn.wait();
+ self.portlet.disable();
+ self.submit( options, function() {
+ execute_btn.unwait();
+ self.portlet.enable();
+ } );
+ }
+ })
+ };
+ // remap feature
+ if ( options.job_id && options.job_remap ) {
+ options.inputs[ 'rerun_remap_job_id' ] = {
+ label : 'Resume dependencies from this job',
+ name : 'rerun_remap_job_id',
+ type : 'select',
+ display : 'radio',
+ ignore : '__ignore__',
+ value : '__ignore__',
+ options : [ [ 'Yes', options.job_id ], [ 'No', '__ignore__' ] ],
+ help : 'The previous run of this tool failed and other tools were waiting for it to finish successfully. Use this option to resume those tools using the new output(s) of this tool run.'
+ }
+ }
+ }
+ }, options ) );
+ },
+
+ /** Submit a regular job.
+ * @param{dict} options - Specifies tool id and version
+ * @param{function} callback - Called when request has completed
+ */
+ submit: function( options, callback ) {
+ var self = this;
+ var job_def = {
+ tool_id : options.id,
+ tool_version : options.version,
+ inputs : this.data.create()
+ }
+ this.trigger( 'reset' );
+ if ( !self.validate( job_def ) ) {
+ Galaxy.emit.debug( 'tool-form::submit()', 'Submission canceled. Validation failed.' );
+ callback && callback();
+ return;
+ }
+ Galaxy.emit.debug( 'tool-form::submit()', 'Validation complete.', job_def );
+ Utils.request({
+ type : 'POST',
+ url : Galaxy.root + 'api/tools',
+ data : job_def,
+ success : function( response ) {
+ callback && callback();
+ self.$el.empty().append( ToolTemplate.success( response ) );
+ parent.Galaxy && parent.Galaxy.currHistoryPanel && parent.Galaxy.currHistoryPanel.refreshContents();
+ },
+ error : function( response ) {
+ callback && callback();
+ Galaxy.emit.debug( 'tool-form::submit', 'Submission failed.', response );
+ if ( response && response.err_data ) {
+ var error_messages = self.data.matchResponse( response.err_data );
+ for (var input_id in error_messages) {
+ self.highlight( input_id, error_messages[ input_id ]);
+ break;
+ }
+ } else {
+ self.modal.show({
+ title : 'Job submission failed',
+ body : ( response && response.err_msg ) || ToolTemplate.error( job_def ),
+ buttons : {
+ 'Close' : function() {
+ self.modal.hide();
+ }
+ }
+ });
+ }
+ }
+ });
+ },
+
+ /** Validate job dictionary.
+ * @param{dict} job_def - Job execution dictionary
+ */
+ validate: function( job_def ) {
+ var job_inputs = job_def.inputs;
+ var batch_n = -1;
+ var batch_src = null;
+ for ( var job_input_id in job_inputs ) {
+ var input_value = job_inputs[ job_input_id ];
+ var input_id = this.data.match( job_input_id );
+ var input_field = this.field_list[ input_id ];
+ var input_def = this.input_list[ input_id ];
+ if ( !input_id || !input_def || !input_field ) {
+ Galaxy.emit.debug('tool-form::validate()', 'Retrieving input objects failed.');
+ continue;
+ }
+ if ( !input_def.optional && input_value == null ) {
+ this.highlight( input_id );
+ return false;
+ }
+ if ( input_value && input_value.batch ) {
+ var n = input_value.values.length;
+ var src = n > 0 && input_value.values[ 0 ] && input_value.values[ 0 ].src;
+ if ( src ) {
+ if ( batch_src === null ) {
+ batch_src = src;
+ } else if ( batch_src !== src ) {
+ this.highlight( input_id, 'Please select either dataset or dataset list fields for all batch mode fields.' );
+ return false;
+ }
+ }
+ if ( batch_n === -1 ) {
+ batch_n = n;
+ } else if ( batch_n !== n ) {
+ this.highlight( input_id, 'Please make sure that you select the same number of inputs for all batch mode fields. This field contains ' + n + ' selection(s) while a previous field contains ' + batch_n + '.' );
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+ });
+
+ return {
+ View: View
+ };
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 21 */,
+/* 22 */
+/*!******************************************!*\
+ !*** ./galaxy/scripts/mvc/ui/ui-misc.js ***!
+ \******************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(Backbone, $) {/**
+ * This class contains backbone wrappers for basic ui elements such as Images, Labels, Buttons, Input fields etc.
+ */
+ !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! utils/utils */ 21),
+ __webpack_require__(/*! mvc/ui/ui-select-default */ 23),
+ __webpack_require__(/*! mvc/ui/ui-slider */ 25),
+ __webpack_require__(/*! mvc/ui/ui-options */ 26),
+ __webpack_require__(/*! mvc/ui/ui-drilldown */ 27),
+ __webpack_require__(/*! mvc/ui/ui-buttons */ 24),
+ __webpack_require__(/*! mvc/ui/ui-modal */ 17)], __WEBPACK_AMD_DEFINE_RESULT__ = function( Utils, Select, Slider, Options, Drilldown, Buttons, Modal ) {
+
+ /** Label wrapper */
+ var Label = Backbone.View.extend({
+ tagName: 'label',
+ initialize: function( options ) {
+ this.model = options && options.model || new Backbone.Model( options );
+ this.tagName = options.tagName || this.tagName;
+ this.setElement( $( '<' + this.tagName + '/>' ) );
+ this.listenTo( this.model, 'change', this.render, this );
+ this.render();
+ },
+ title: function( new_title ) {
+ this.model.set( 'title', new_title );
+ },
+ value: function() {
+ return this.model.get( 'title' );
+ },
+ render: function() {
+ this.$el.removeClass()
+ .addClass( 'ui-label' )
+ .addClass( this.model.get( 'cls' ) )
+ .html( this.model.get( 'title' ) );
+ return this;
+ }
+ });
+
+ /** Displays messages used e.g. in the tool form */
+ var Message = Backbone.View.extend({
+ initialize: function( options ) {
+ this.model = options && options.model || new Backbone.Model({
+ message : null,
+ status : 'info',
+ cls : '',
+ persistent : false
+ }).set( options );
+ this.listenTo( this.model, 'change', this.render, this );
+ this.render();
+ },
+ update: function( options ) {
+ this.model.set( options );
+ },
+ render: function() {
+ this.$el.removeClass().addClass( 'ui-message' ).addClass( this.model.get( 'cls' ) );
+ var status = this.model.get( 'status' );
+ if ( this.model.get( 'large' ) ) {
+ this.$el.addClass((( status == 'success' && 'done' ) ||
+ ( status == 'danger' && 'error' ) ||
+ status ) + 'messagelarge' );
+ } else {
+ this.$el.addClass( 'alert' ).addClass( 'alert-' + status );
+ }
+ if ( this.model.get( 'message' ) ) {
+ this.$el.html( this.model.get( 'message' ) );
+ this.$el.fadeIn();
+ this.timeout && window.clearTimeout( this.timeout );
+ if ( !this.model.get( 'persistent' ) ) {
+ var self = this;
+ this.timeout = window.setTimeout( function() {
+ self.$el.fadeOut();
+ }, 3000 );
+ }
+ } else {
+ this.$el.fadeOut();
+ }
+ return this;
+ }
+ });
+
+ /** Renders an input element used e.g. in the tool form */
+ var Input = Backbone.View.extend({
+ initialize: function( options ) {
+ this.model = options && options.model || new Backbone.Model({
+ type : 'text',
+ placeholder : '',
+ disabled : false,
+ visible : true,
+ cls : '',
+ area : false
+ }).set( options );
+ this.tagName = this.model.get( 'area' ) ? 'textarea' : 'input';
+ this.setElement( $( '<' + this.tagName + '/>' ) );
+ this.listenTo( this.model, 'change', this.render, this );
+ this.render();
+ },
+ events: {
+ 'input': '_onchange'
+ },
+ value: function( new_val ) {
+ new_val !== undefined && this.model.set( 'value', typeof new_val === 'string' ? new_val : '' );
+ return this.model.get( 'value' );
+ },
+ render: function() {
+ this.$el.removeClass()
+ .addClass( 'ui-' + this.tagName )
+ .addClass( this.model.get( 'cls' ) )
+ .attr( 'id', this.model.id )
+ .attr( 'type', this.model.get( 'type' ) )
+ .attr( 'placeholder', this.model.get( 'placeholder' ) )
+ .val( this.model.get( 'value' ) );
+ this.model.get( 'disabled' ) ? this.$el.attr( 'disabled', true ) : this.$el.removeAttr( 'disabled' );
+ this.$el[ this.model.get( 'visible' ) ? 'show' : 'hide' ]();
+ return this;
+ },
+ _onchange: function() {
+ this.value( this.$el.val() );
+ this.model.get( 'onchange' ) && this.model.get( 'onchange' )( this.model.get( 'value' ) );
+ }
+ });
+
+ /** Creates a hidden element input field used e.g. in the tool form */
+ var Hidden = Backbone.View.extend({
+ initialize: function( options ) {
+ this.model = options && options.model || new Backbone.Model( options );
+ this.setElement( $ ( '' ).append( this.$info = $( '' ) )
+ .append( this.$hidden = $( '' ) ) );
+ this.listenTo( this.model, 'change', this.render, this );
+ this.render();
+ },
+ value: function( new_val ) {
+ new_val !== undefined && this.model.set( 'value', new_val );
+ return this.model.get( 'value' );
+ },
+ render: function() {
+ this.$el.attr( 'id', this.model.id );
+ this.$hidden.val( this.model.get( 'value' ) );
+ this.model.get( 'info' ) ? this.$info.show().html( this.model.get( 'info' ) ) : this.$info.hide();
+ return this;
+ }
+ });
+
+ return {
+ Button : Buttons.ButtonDefault,
+ ButtonIcon : Buttons.ButtonIcon,
+ ButtonCheck : Buttons.ButtonCheck,
+ ButtonMenu : Buttons.ButtonMenu,
+ ButtonLink : Buttons.ButtonLink,
+ Input : Input,
+ Label : Label,
+ Message : Message,
+ Modal : Modal,
+ RadioButton : Options.RadioButton,
+ Checkbox : Options.Checkbox,
+ Radio : Options.Radio,
+ Select : Select,
+ Hidden : Hidden,
+ Slider : Slider,
+ Drilldown : Drilldown
+ }
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! libs/backbone */ 2), __webpack_require__(/*! jquery */ 3)))
+
+/***/ },
+/* 23 */
+/*!****************************************************!*\
+ !*** ./galaxy/scripts/mvc/ui/ui-select-default.js ***!
+ \****************************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(Backbone, $, _) {/**
+ * This class creates/wraps a default html select field as backbone class.
+ */
+ !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! utils/utils */ 21), __webpack_require__(/*! mvc/ui/ui-buttons */ 24)], __WEBPACK_AMD_DEFINE_RESULT__ = function(Utils, Buttons) {
+ var View = Backbone.View.extend({
+ // options
+ optionsDefault: {
+ id : Utils.uid(),
+ cls : 'ui-select',
+ error_text : 'No options available',
+ empty_text : 'Nothing selected',
+ visible : true,
+ wait : false,
+ multiple : false,
+ searchable : true,
+ optional : false
+ },
+
+ // initialize
+ initialize: function(options) {
+ // link this
+ var self = this;
+ this.model = new Backbone.Model();
+
+ // configure options
+ this.options = Utils.merge(options, this.optionsDefault);
+
+ // create new element
+ this.setElement(this._template(this.options));
+
+ // link elements
+ this.$select = this.$el.find('.select');
+ this.$icon_dropdown = this.$el.find('.icon-dropdown');
+ this.$icon_dropdown.on( 'click', function() { self.$select.select2 && self.$select.select2( 'open' ) });
+
+ // allow regular multi-select field to be resized
+ var minHeight = null;
+ this.$('.icon-resize').on('mousedown', function(event) {
+ var currentY = event.pageY;
+ var currentHeight = self.$select.height();
+ minHeight = minHeight || currentHeight;
+ $('#dd-helper').show().on('mousemove', function(event) {
+ self.$select.height(Math.max(currentHeight + (event.pageY - currentY), minHeight));
+ }).on('mouseup mouseleave', function() {
+ $('#dd-helper').hide().off();
+ });
+ });
+
+ // multiple select fields have an additional button and other custom properties
+ if (this.options.multiple) {
+ // create select all button
+ if (this.options.searchable) {
+ this.all_button = new Buttons.ButtonCheck({
+ onclick: function() {
+ var new_value = [];
+ if (self.all_button.value() !== 0) {
+ new_value = self._availableOptions();
+ }
+ self.value(new_value);
+ self.trigger('change');
+ }
+ });
+ this.$el.prepend(this.all_button.$el);
+ } else {
+ this.$el.addClass('ui-select-multiple');
+ }
+ this.$select.prop('multiple', true);
+ this.$icon_dropdown.remove();
+ }
+
+ // update initial options
+ this.update(this.options.data);
+
+ // set initial value
+ if (this.options.value !== undefined) {
+ this.value(this.options.value);
+ }
+
+ // show/hide
+ if (!this.options.visible) {
+ this.hide();
+ }
+
+ // wait
+ if (this.options.wait) {
+ this.wait();
+ } else {
+ this.show();
+ }
+
+ // add change event. fires only on user activity
+ this.$select.on('change', function() {
+ self.trigger('change');
+ });
+
+ // add change event. fires on trigger
+ this.on('change', function() {
+ self.options.onchange && self.options.onchange(this.value());
+ });
+ },
+
+ /** Return/Set current selection
+ */
+ value: function (new_value) {
+ // set new value
+ if (new_value !== undefined) {
+ if (new_value === null) {
+ new_value = '__null__';
+ }
+ if (this.exists(new_value) || this.options.multiple) {
+ this.$select.val(new_value);
+ if (this.$select.select2) {
+ this.$select.select2('val', new_value);
+ }
+ }
+ }
+ // get current value
+ var current = this._getValue();
+ if (this.all_button) {
+ this.all_button.value($.isArray(current) && current.length || 0, this._size());
+ }
+ return current;
+ },
+
+ /** Return the first select option
+ */
+ first: function() {
+ var options = this.$select.find('option').first();
+ if (options.length > 0) {
+ return options.val();
+ } else {
+ return null;
+ }
+ },
+
+ /** Return the label/text of the current selection
+ */
+ text: function () {
+ return this.$select.find('option:selected').text();
+ },
+
+ /** Show the select field
+ */
+ show: function() {
+ this.unwait();
+ this.$select.show();
+ this.$el.show();
+ },
+
+ /** Hide the select field
+ */
+ hide: function() {
+ this.$el.hide();
+ },
+
+ /** Show a spinner indicating that the select options are currently loaded
+ */
+ wait: function() {
+ this.$icon_dropdown.removeClass();
+ this.$icon_dropdown.addClass('icon-dropdown fa fa-spinner fa-spin');
+ },
+
+ /** Hide spinner indicating that the request has been completed
+ */
+ unwait: function() {
+ this.$icon_dropdown.removeClass();
+ this.$icon_dropdown.addClass('icon-dropdown fa fa-caret-down');
+ },
+
+ /** Returns true if the field is disabled
+ */
+ disabled: function() {
+ return this.$select.is(':disabled');
+ },
+
+ /** Enable the select field
+ */
+ enable: function() {
+ this.$select.prop('disabled', false);
+ },
+
+ /** Disable the select field
+ */
+ disable: function() {
+ this.$select.prop('disabled', true);
+ },
+
+ /** Update all available options at once
+ */
+ add: function( options, sorter ) {
+ _.each( this.model.get( 'options' ), function( v ) {
+ !_.findWhere( options, v ) && options.push( v );
+ });
+ sorter && options && options.sort( sorter );
+ this.update( options );
+ },
+ update: function(options) {
+ // backup current value
+ var current = this._getValue();
+
+ // remove all options
+ this.$select.find('option').remove();
+
+ // add optional field
+ if (!this.options.multiple && this.options.optional) {
+ this.$select.append(this._templateOption({value : '__null__', label : this.options.empty_text}));
+ }
+
+ // add new options
+ this.model.set( 'options', options );
+ for (var key in options) {
+ this.$select.append(this._templateOption(options[key]));
+ }
+
+ // count remaining entries
+ if (this._size() == 0) {
+ // disable select field
+ this.disable();
+
+ // create placeholder
+ this.$select.append(this._templateOption({value : '__null__', label : this.options.error_text}));
+ } else {
+ // enable select field
+ this.enable();
+ }
+
+ // update to searchable field (in this case select2)
+ if (this.options.searchable) {
+ this.$select.select2('destroy');
+ this.$select.select2({ closeOnSelect: !this.options.multiple });
+ this.$( '.select2-container .select2-search input' ).off( 'blur' );
+ }
+
+ // set previous value
+ this.value(current);
+
+ // check if any value was set
+ if (this._getValue() === null && !(this.options.multiple && this.options.optional)) {
+ this.value(this.first());
+ }
+ },
+
+ /** Set the custom onchange callback function
+ */
+ setOnChange: function(callback) {
+ this.options.onchange = callback;
+ },
+
+ /** Check if a value is an existing option
+ */
+ exists: function(value) {
+ return this.$select.find('option[value="' + value + '"]').length > 0;
+ },
+
+ /** Get current value from dom
+ */
+ _getValue: function() {
+ var val = this.$select.val();
+ if (!Utils.validate(val)) {
+ return null;
+ }
+ return val;
+ },
+
+ /** Returns all currently available options
+ */
+ _availableOptions: function() {
+ var available = [];
+ this.$select.find('option').each(function(i, e){
+ available.push($(e).attr('value'));
+ });
+ return available;
+ },
+
+ /** Number of available options
+ */
+ _size: function() {
+ return this.$select.find('option').length;
+ },
+
+ /** Template for select options
+ */
+ _templateOption: function(options) {
+ return '';
+ },
+
+ /** Template for select view
+ */
+ _template: function(options) {
+ return '
' +
+ '' +
+ '' +
+ '
' +
+ '' +
+ '
' +
+ '
';
+ }
+ });
+
+ return {
+ View: View
+ }
+
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! libs/backbone */ 2), __webpack_require__(/*! jquery */ 3), __webpack_require__(/*! underscore */ 1)))
+
+/***/ },
+/* 24 */
+/*!*********************************************!*\
+ !*** ./galaxy/scripts/mvc/ui/ui-buttons.js ***!
+ \*********************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(Backbone, $) {/** This class contains all button views.
+ */
+ !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! utils/utils */ 21)], __WEBPACK_AMD_DEFINE_RESULT__ = function( Utils ) {
+ /** This renders the default button which is used e.g. at the bottom of the upload modal.
+ */
+ var ButtonBase = Backbone.View.extend({
+ initialize: function( options ) {
+ this.options = Utils.merge( options, {
+ id : Utils.uid(),
+ title : '',
+ floating : 'right',
+ icon : '',
+ cls : 'ui-button btn btn-default',
+ cls_wait : 'btn btn-info'
+ } );
+ this.setElement( this._template( this.options ) );
+ var self = this;
+ $( this.el ).on( 'click' , function() {
+ $( '.tooltip' ).hide();
+ if ( options.onclick && !self.disabled ) {
+ options.onclick();
+ }
+ } );
+ $( this.el ).tooltip( { title: options.tooltip, placement: 'bottom' } );
+ },
+
+ // disable
+ disable: function() {
+ this.$el.addClass( 'disabled' );
+ this.disabled = true;
+ },
+
+ // enable
+ enable: function() {
+ this.$el.removeClass( 'disabled' );
+ this.disabled = false;
+ },
+
+ // show spinner
+ wait: function() {
+ this.$el.removeClass( this.options.cls ).addClass( this.options.cls_wait ).prop( 'disabled', true );
+ this.$( '.icon' ).removeClass( this.options.icon ).addClass( 'fa-spinner fa-spin' );
+ this.$( '.title' ).html( 'Sending...' );
+ },
+
+ // hide spinner
+ unwait: function() {
+ this.$el.removeClass( this.options.cls_wait ).addClass( this.options.cls ).prop( 'disabled', false );
+ this.$( '.icon' ).removeClass( 'fa-spinner fa-spin' ).addClass( this.options.icon );
+ this.$( '.title' ).html( this.options.title );
+ },
+
+ // template
+ _template: function( options ) {
+ var str = '';
+ return str;
+ }
+ });
+
+ /** This button allows the right-click/open-in-new-tab feature, its used e.g. for panel buttons.
+ */
+ var ButtonLink = ButtonBase.extend({
+ initialize: function( options ) {
+ ButtonBase.prototype.initialize.call( this, options );
+ },
+ _template: function( options ) {
+ return '' + '' +
+ '';
+ }
+ });
+
+ /** The check button is used in the tool form and allows to distinguish between multiple states e.g. all, partially and nothing selected.
+ */
+ var ButtonCheck = Backbone.View.extend({
+ initialize: function( options ) {
+ // configure options
+ this.options = Utils.merge(options, {
+ title : 'Select/Unselect all',
+ icons : ['fa fa-square-o', 'fa fa-minus-square-o', 'fa fa-check-square-o'],
+ value : 0
+ });
+
+ // create new element
+ this.setElement( this._template() );
+ this.$title = this.$( '.title' );
+ this.$icon = this.$( '.icon' );
+
+ // set initial value
+ this.value( this.options.value );
+
+ // set title
+ this.$title.html( this.options.title );
+
+ // add event handler
+ var self = this;
+ this.$el.on('click', function() {
+ self.current = ( self.current === 0 && 2 ) || 0;
+ self.value( self.current );
+ self.options.onclick && self.options.onclick();
+ });
+ },
+
+ /* Sets a new value and/or returns the current value.
+ * @param{Integer} new_val - Set a new value 0=unchecked, 1=partial and 2=checked.
+ * OR:
+ * @param{Integer} new_val - Number of selected options.
+ * @param{Integer} total - Total number of available options.
+ */
+ value: function ( new_val, total ) {
+ if ( new_val !== undefined ) {
+ if ( total ) {
+ if ( new_val !== 0 ) {
+ new_val = ( new_val !== total ) && 1 || 2;
+ }
+ }
+ this.current = new_val;
+ this.$icon.removeClass()
+ .addClass( 'icon' )
+ .addClass( this.options.icons[ new_val ] );
+ this.options.onchange && this.options.onchange( new_val );
+ }
+ return this.current;
+ },
+
+ /** Template containing the check button and the title
+ */
+ _template: function() {
+ return '
You can check the status of queued jobs and view the resulting data by refreshing the History pane. When the job has been run the status will change from \'running\' to \'finished\' if completed successfully or \'error\' if problems were encountered.
',
+ 'Warning: This is a experimental feature. Most Galaxy tools will not annotate',
+ ' citations explicitly at this time. When writing up your analysis, please manually',
+ ' review your histories and find all references',
+ ' that should be cited in order to completely describe your work. Also, please remember to',
+ ' cite Galaxy.',
+ '
This Galaxy server allows you to upload files via FTP. To upload some files, log in to the FTP server at ' + this.options.ftp_upload_site + ' using your Galaxy credentials (email address and password).
' +
+ '' +
+ '
';
+ }
+ });
+
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! libs/backbone */ 2), __webpack_require__(/*! jquery */ 3)))
+
+/***/ },
+/* 57 */
+/*!*******************************************!*\
+ !*** ./galaxy/scripts/utils/uploadbox.js ***!
+ \*******************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ /* WEBPACK VAR INJECTION */(function(jQuery, _) {/*
+ galaxy upload plugins - requires FormData and XMLHttpRequest
+ */
+ ;(function($){
+ // add event properties
+ jQuery.event.props.push("dataTransfer");
+
+ /**
+ Posts file data to the API
+ */
+ $.uploadpost = function (config) {
+ // parse options
+ var cnf = $.extend({}, {
+ data : {},
+ success : function() {},
+ error : function() {},
+ progress : function() {},
+ url : null,
+ maxfilesize : 2048,
+ error_filesize : 'File exceeds 2GB. Please use a FTP client.',
+ error_default : 'Please make sure the file is available.',
+ error_server : 'Upload request failed.',
+ error_login : 'Uploads require you to log in.'
+ }, config);
+
+ // link data
+ var data = cnf.data;
+
+ // check errors
+ if (data.error_message) {
+ cnf.error(data.error_message);
+ return;
+ }
+
+ // construct form data
+ var form = new FormData();
+ for (var key in data.payload) {
+ form.append(key, data.payload[key]);
+ }
+
+ // add files to submission
+ var sizes = 0;
+ for (var key in data.files) {
+ var d = data.files[key];
+ form.append(d.name, d.file, d.file.name);
+ sizes += d.file.size;
+ }
+
+ // check file size, unless it's an ftp file
+ if (sizes > 1048576 * cnf.maxfilesize) {
+ cnf.error(cnf.error_filesize);
+ return;
+ }
+
+ // prepare request
+ xhr = new XMLHttpRequest();
+ xhr.open('POST', cnf.url, true);
+ xhr.setRequestHeader('Accept', 'application/json');
+ xhr.setRequestHeader('Cache-Control', 'no-cache');
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
+
+ // captures state changes
+ xhr.onreadystatechange = function() {
+ // check for request completed, server connection closed
+ if (xhr.readyState == xhr.DONE) {
+ // parse response
+ var response = null;
+ if (xhr.responseText) {
+ try {
+ response = jQuery.parseJSON(xhr.responseText);
+ } catch (e) {
+ response = xhr.responseText;
+ }
+ }
+ // pass any error to the error option
+ if (xhr.status < 200 || xhr.status > 299) {
+ var text = xhr.statusText;
+ if (xhr.status == 403) {
+ text = cnf.error_login;
+ } else if (xhr.status == 0) {
+ text = cnf.error_server;
+ } else if (!text) {
+ text = cnf.error_default;
+ }
+ cnf.error(text + ' (' + xhr.status + ')');
+ } else {
+ cnf.success(response);
+ }
+ }
+ }
+
+ // prepare upload progress
+ xhr.upload.addEventListener('progress', function(e) {
+ if (e.lengthComputable) {
+ cnf.progress(Math.round((e.loaded * 100) / e.total));
+ }
+ }, false);
+
+ // send request
+ Galaxy.emit.debug('uploadbox::uploadpost()', 'Posting following data.', cnf);
+ xhr.send(form);
+ }
+
+ /**
+ Handles the upload events drag/drop etc.
+ */
+ $.fn.uploadinput = function(options) {
+ // initialize
+ var el = this;
+ var opts = $.extend({}, {
+ ondragover : function() {},
+ ondragleave : function() {},
+ onchange : function() {},
+ multiple : false
+ }, options);
+
+ // append hidden upload field
+ var $input = $('');
+ el.append($input.change(function (e) {
+ opts.onchange(e.target.files);
+ $(this).val('');
+ }));
+
+ // drag/drop events
+ el.on('drop', function (e) {
+ opts.ondragleave(e);
+ if(e.dataTransfer) {
+ opts.onchange(e.dataTransfer.files);
+ e.preventDefault();
+ }
+ });
+ el.on('dragover', function (e) {
+ e.preventDefault();
+ opts.ondragover(e);
+ });
+ el.on('dragleave', function (e) {
+ e.stopPropagation();
+ opts.ondragleave(e);
+ });
+
+ // exports
+ return {
+ dialog: function () {
+ $input.trigger('click');
+ }
+ }
+ }
+
+ /**
+ Handles the upload queue and events such as drag/drop etc.
+ */
+ $.fn.uploadbox = function(options) {
+ // parse options
+ var opts = $.extend({}, {
+ dragover : function() {},
+ dragleave : function() {},
+ announce : function(d) {},
+ initialize : function(d) {},
+ progress : function(d, m) {},
+ success : function(d, m) {},
+ error : function(d, m) { alert(m); },
+ complete : function() {}
+ }, options);
+
+ // file queue
+ var queue = {};
+
+ // queue index/length counter
+ var queue_index = 0;
+ var queue_length = 0;
+
+ // indicates if queue is currently running
+ var queue_running = false;
+ var queue_stop = false;
+
+ // element
+ var uploadinput = $(this).uploadinput({
+ multiple : true,
+ onchange : function(files) { add(files); },
+ ondragover : options.ondragover,
+ ondragleave : options.ondragleave
+ });
+
+ // add new files to upload queue
+ function add(files) {
+ if (files && files.length && !queue_running) {
+ var current_index = queue_index;
+ _.each(files, function(file, key) {
+ if (file.mode !== 'new' && _.filter(queue, function(f) {
+ return f.name === file.name && f.size === file.size;
+ }).length) {
+ file.duplicate = true;
+ }
+ });
+ _.each(files, function(file) {
+ if (!file.duplicate) {
+ var index = String(queue_index++);
+ queue[index] = file;
+ opts.announce(index, queue[index]);
+ queue_length++;
+ }
+ });
+ return current_index;
+ }
+ }
+
+ // remove file from queue
+ function remove(index) {
+ if (queue[index]) {
+ delete queue[index];
+ queue_length--;
+ }
+ }
+
+ // process an upload, recursive
+ function process() {
+ // validate
+ if (queue_length == 0 || queue_stop) {
+ queue_stop = false;
+ queue_running = false;
+ opts.complete();
+ return;
+ } else {
+ queue_running = true;
+ }
+
+ // get an identifier from the queue
+ var index = -1;
+ for (var key in queue) {
+ index = key;
+ break;
+ }
+
+ // get current file from queue
+ var file = queue[index];
+
+ // remove from queue
+ remove(index)
+
+ // create and submit data
+ $.uploadpost({
+ url : opts.url,
+ data : opts.initialize(index),
+ success : function(message) { opts.success(index, message); process();},
+ error : function(message) { opts.error(index, message); process();},
+ progress : function(percentage) { opts.progress(index, percentage); }
+ });
+ }
+
+ /*
+ public interface
+ */
+
+ // open file browser for selection
+ function select() {
+ uploadinput.dialog();
+ }
+
+ // remove all entries from queue
+ function reset(index) {
+ for (index in queue) {
+ remove(index);
+ }
+ }
+
+ // initiate upload process
+ function start() {
+ if (!queue_running) {
+ queue_running = true;
+ process();
+ }
+ }
+
+ // stop upload process
+ function stop() {
+ queue_stop = true;
+ }
+
+ // set options
+ function configure(options) {
+ opts = $.extend({}, opts, options);
+ return opts;
+ }
+
+ // verify browser compatibility
+ function compatible() {
+ return window.File && window.FormData && window.XMLHttpRequest && window.FileList;
+ }
+
+ // export functions
+ return {
+ 'select' : select,
+ 'add' : add,
+ 'remove' : remove,
+ 'start' : start,
+ 'stop' : stop,
+ 'reset' : reset,
+ 'configure' : configure,
+ 'compatible' : compatible
+ };
+ }
+ })(jQuery);
+
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! jquery */ 3), __webpack_require__(/*! underscore */ 1)))
+
+/***/ },
+/* 58 */
+/*!***************************************************************!*\
+ !*** ./galaxy/scripts/mvc/upload/composite/composite-view.js ***!
+ \***************************************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(Backbone, _, $) {/** Renders contents of the composite uploader */
+ !(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(/*! utils/utils */ 21),
+ __webpack_require__(/*! mvc/upload/upload-model */ 51),
+ __webpack_require__(/*! mvc/upload/composite/composite-row */ 59),
+ __webpack_require__(/*! mvc/ui/ui-popover */ 54),
+ __webpack_require__(/*! mvc/ui/ui-select */ 55),
+ __webpack_require__(/*! mvc/ui/ui-misc */ 22)], __WEBPACK_AMD_DEFINE_RESULT__ = function( Utils,
+ UploadModel,
+ UploadRow,
+ Popover,
+ Select,
+ Ui
+ ) {
+
+ return Backbone.View.extend({
+ // extension selector
+ select_extension: null,
+
+ // genome selector
+ select_genome: null,
+
+ // collection
+ collection: new UploadModel.Collection(),
+
+ // initialize
+ initialize: function(app) {
+ // link app
+ this.app = app;
+ this.options = app.options;
+ this.list_extensions = app.list_extensions;
+ this.list_genomes = app.list_genomes;
+ this.ftp_upload_site = app.currentFtp();
+
+ // link this
+ var self = this;
+
+ // set element
+ this.setElement(this._template());
+
+ // create button section
+ this.btnStart = new Ui.Button({ title: 'Start', onclick: function() { self._eventStart(); } });
+ this.btnClose = new Ui.Button({ title: 'Close', onclick: function() { self.app.modal.hide(); } });
+
+ // append buttons to dom
+ var buttons = [ this.btnStart, this.btnClose ];
+ for (var i in buttons) {
+ this.$('#upload-buttons').prepend(buttons[i].$el);
+ }
+
+ // select extension
+ this.select_extension = new Select.View({
+ css : 'footer-selection',
+ container : this.$('#footer-extension'),
+ data : _.filter(this.list_extensions, function(ext) { return ext.composite_files }),
+ onchange : function(extension) {
+ self.collection.reset();
+ var details = _.findWhere(self.list_extensions, { id : extension });
+ if (details && details.composite_files) {
+ for (var i in details.composite_files) {
+ var item = details.composite_files[i];
+ self.collection.add({
+ id : self.collection.size(),
+ file_desc : item['description'] || item['name']
+ });
+ }
+ }
+ }
+ });
+
+ // handle extension info popover
+ this.$('#footer-extension-info').on('click', function(e) {
+ self._showExtensionInfo({
+ $el : $(e.target),
+ title : self.select_extension.text(),
+ extension : self.select_extension.value(),
+ placement : 'top'
+ });
+ }).on('mousedown', function(e) { e.preventDefault(); });
+
+ // genome extension
+ this.select_genome = new Select.View({
+ css : 'footer-selection',
+ container : this.$('#footer-genome'),
+ data : this.list_genomes,
+ value : this.options.default_genome
+ });
+
+ // listener for collection triggers on change in composite datatype
+ this.collection.on('add', function (model) {
+ self._eventAnnounce(model);
+ });
+ this.collection.on('change add', function() {
+ self._updateScreen();
+ }).trigger('change');
+
+ // trigger initial onchange event
+ this.select_extension.options.onchange(this.select_extension.value());
+ },
+
+ //
+ // upload events / process pipeline
+ //
+
+ // builds the basic ui with placeholder rows for each composite data type file
+ _eventAnnounce: function(model) {
+ // create view/model
+ var upload_row = new UploadRow(this, { model : model });
+
+ // add upload row element to table
+ this.$('#upload-table > tbody:first').append(upload_row.$el);
+
+ // render
+ upload_row.render();
+
+ // table visibility
+ if (this.collection.length > 0) {
+ this.$('#upload-table').show();
+ } else {
+ this.$('#upload-table').hide();
+ }
+ },
+
+ // start upload process
+ _eventStart: function() {
+ var self = this;
+ this.collection.each(function(model) {
+ model.set('genome', self.select_genome.value());
+ model.set('extension', self.select_extension.value());
+ });
+ $.uploadpost({
+ url : this.app.options.nginx_upload_path,
+ data : this.app.toData(this.collection.filter()),
+ success : function(message) { self._eventSuccess(message); },
+ error : function(message) { self._eventError(message); },
+ progress : function(percentage) { self._eventProgress(percentage); }
+ });
+ },
+
+ // progress
+ _eventProgress: function(percentage) {
+ this.collection.each(function(it) { it.set('percentage', percentage); });
+ },
+
+ // success
+ _eventSuccess: function(message) {
+ this.collection.each(function(it) {
+ it.set('status', 'success');
+ });
+ Galaxy.currHistoryPanel.refreshContents();
+ },
+
+ // error
+ _eventError: function(message) {
+ this.collection.each(function(it) {
+ it.set('status', 'error');
+ it.set('info', message);
+ });
+ },
+
+ // display extension info popup
+ _showExtensionInfo: function(options) {
+ // initialize
+ var self = this;
+ var $el = options.$el;
+ var extension = options.extension;
+ var title = options.title;
+ var description = _.findWhere(this.list_extensions, { id : extension });
+
+ // create popup
+ this.extension_popup && this.extension_popup.remove();
+ this.extension_popup = new Popover.View({
+ placement: options.placement || 'bottom',
+ container: $el,
+ destroy: true
+ });
+
+ // add content and show popup
+ this.extension_popup.title(title);
+ this.extension_popup.empty();
+ this.extension_popup.append(this._templateDescription(description));
+ this.extension_popup.show();
+ },
+
+ // set screen
+ _updateScreen: function () {
+ // show start button if components have been selected
+ var model = this.collection.first();
+ if (model && model.get('status') == 'running') {
+ this.select_genome.disable();
+ this.select_extension.disable();
+ } else {
+ this.select_genome.enable();
+ this.select_extension.enable();
+ }
+ if (this.collection.where({ status : 'ready' }).length == this.collection.length && this.collection.length > 0) {
+ this.btnStart.enable();
+ this.btnStart.$el.addClass('btn-primary');
+ } else {
+ this.btnStart.disable();
+ this.btnStart.$el.removeClass('btn-primary');
+ }
+
+ // table visibility
+ if (this.collection.length > 0) {
+ this.$('#upload-table').show();
+ } else {
+ this.$('#upload-table').hide();
+ }
+ },
+
+ // template for extensions description
+ _templateDescription: function(options) {
+ if (options.description) {
+ var tmpl = options.description;
+ if (options.description_url) {
+ tmpl += ' (read more)';
+ }
+ return tmpl;
+ } else {
+ return 'There is no description available for this file extension.';
+ }
+ },
+
+ // load html template
+ _template: function() {
+ return '
'
+ ].join( '' );
+ }).join( '' );
+ },
+
+ // get the absolute position/offset for the menu
+ _getShownPosition : function( clickEvent ){
+
+ // display menu horiz. centered on click...
+ var menuWidth = this.$el.width();
+ var x = clickEvent.pageX - menuWidth / 2 ;
+
+ // adjust to handle horiz. scroll and window dimensions ( draw entirely on visible screen area )
+ x = Math.min( x, $( document ).scrollLeft() + $( window ).width() - menuWidth - 5 );
+ x = Math.max( x, $( document ).scrollLeft() + 5 );
+ return {
+ top: clickEvent.pageY,
+ left: x
+ };
+ },
+
+ // bind an event handler to all available frames so that when anything is clicked
+ // the menu is removed from the DOM and the event handler unbinds itself
+ _setUpCloseBehavior: function(){
+ var menu = this;
+ //TODO: alternately: focus hack, blocking overlay, jquery.blockui
+
+ // function to close popup and unbind itself
+ function closePopup( event ){
+ $( document ).off( 'click.close_popup' );
+ if( window && window.parent !== window ){
+ try {
+ $( window.parent.document ).off( "click.close_popup" );
+ } catch( err ){}
+ } else {
+ try {
+ $( 'iframe#galaxy_main' ).contents().off( "click.close_popup" );
+ } catch( err ){}
+ }
+ menu.remove();
+ }
+
+ $( 'html' ).one( "click.close_popup", closePopup );
+ if( window && window.parent !== window ){
+ try {
+ $( window.parent.document ).find( 'html' ).one( "click.close_popup", closePopup );
+ } catch( err ){}
+ } else {
+ try {
+ $( 'iframe#galaxy_main' ).contents().one( "click.close_popup", closePopup );
+ } catch( err ){}
+ }
+ },
+
+ // add a menu option/item at the given index
+ addItem: function( item, index ){
+ // append to end if no index
+ index = ( index >= 0 ) ? index : this.options.length;
+ this.options.splice( index, 0, item );
+ return this;
+ },
+
+ // remove a menu option/item at the given index
+ removeItem: function( index ){
+ if( index >=0 ){
+ this.options.splice( index, 1 );
+ }
+ return this;
+ },
+
+ // search for a menu option by its html
+ findIndexByHtml: function( html ){
+ for( var i = 0; i < this.options.length; i++ ){
+ if( _.has( this.options[i], 'html' ) && ( this.options[i].html === html )){
+ return i;
+ }
+ }
+ return null;
+ },
+
+ // search for a menu option by its html
+ findItemByHtml: function( html ){
+ return this.options[( this.findIndexByHtml( html ))];
+ },
+
+ // string representation
+ toString: function(){
+ return 'PopupMenu';
+ }
+ });
+ /** shortcut to new for when you don't need to preserve the ref */
+ PopupMenu.create = function _create( $button, options ){
+ return new PopupMenu( $button, options );
+ };
+
+ // -----------------------------------------------------------------------------
+ // the following class functions are bridges from the original make_popupmenu and make_popup_menus
+ // to the newer backbone.js PopupMenu
+
+ /** Create a PopupMenu from simple map initial_options activated by clicking button_element.
+ * Converts initial_options to object array used by PopupMenu.
+ * @param {jQuery|DOMElement} button_element element which, when clicked, activates menu
+ * @param {Object} initial_options map of key -> values, where
+ * key is option text, value is fn to call when option is clicked
+ * @returns {PopupMenu} the PopupMenu created
+ */
+ PopupMenu.make_popupmenu = function( button_element, initial_options ){
+ var convertedOptions = [];
+ _.each( initial_options, function( optionVal, optionKey ){
+ var newOption = { html: optionKey };
+
+ // keys with null values indicate: header
+ if( optionVal === null ){ // !optionVal? (null only?)
+ newOption.header = true;
+
+ // keys with function values indicate: a menu option
+ } else if( jQuery.type( optionVal ) === 'function' ){
+ newOption.func = optionVal;
+ }
+ //TODO:?? any other special optionVals?
+ // there was no divider option originally
+ convertedOptions.push( newOption );
+ });
+ return new PopupMenu( $( button_element ), convertedOptions );
+ };
+
+ /** Find all anchors in $parent (using selector) and covert anchors into a PopupMenu options map.
+ * @param {jQuery} $parent the element that contains the links to convert to options
+ * @param {String} selector jq selector string to find links
+ * @returns {Object[]} the options array to initialize a PopupMenu
+ */
+ //TODO: lose parent and selector, pass in array of links, use map to return options
+ PopupMenu.convertLinksToOptions = function( $parent, selector ){
+ $parent = $( $parent );
+ selector = selector || 'a';
+ var options = [];
+ $parent.find( selector ).each( function( elem, i ){
+ var option = {}, $link = $( elem );
+
+ // convert link text to the option text (html) and the href into the option func
+ option.html = $link.text();
+ if( $link.attr( 'href' ) ){
+ var linkHref = $link.attr( 'href' ),
+ linkTarget = $link.attr( 'target' ),
+ confirmText = $link.attr( 'confirm' );
+
+ option.func = function(){
+ // if there's a "confirm" attribute, throw up a confirmation dialog, and
+ // if the user cancels - do nothing
+ if( ( confirmText ) && ( !confirm( confirmText ) ) ){ return; }
+
+ // if there's no confirm attribute, or the user accepted the confirm dialog:
+ switch( linkTarget ){
+ // relocate the center panel
+ case '_parent':
+ window.parent.location = linkHref;
+ break;
+
+ // relocate the entire window
+ case '_top':
+ window.top.location = linkHref;
+ break;
+
+ // relocate this panel
+ default:
+ window.location = linkHref;
+ }
+ };
+ }
+ options.push( option );
+ });
+ return options;
+ };
+
+ /** Create a single popupmenu from existing DOM button and anchor elements
+ * @param {jQuery} $buttonElement the element that when clicked will open the menu
+ * @param {jQuery} $menuElement the element that contains the anchors to convert into a menu
+ * @param {String} menuElementLinkSelector jq selector string used to find anchors to be made into menu options
+ * @returns {PopupMenu} the PopupMenu (Backbone View) that can render, control the menu
+ */
+ PopupMenu.fromExistingDom = function( $buttonElement, $menuElement, menuElementLinkSelector ){
+ $buttonElement = $( $buttonElement );
+ $menuElement = $( $menuElement );
+ var options = PopupMenu.convertLinksToOptions( $menuElement, menuElementLinkSelector );
+ // we're done with the menu (having converted it to an options map)
+ $menuElement.remove();
+ return new PopupMenu( $buttonElement, options );
+ };
+
+ /** Create all popupmenus within a document or a more specific element
+ * @param {DOMElement} parent the DOM element in which to search for popupmenus to build (defaults to document)
+ * @param {String} menuSelector jq selector string to find popupmenu menu elements (defaults to "div[popupmenu]")
+ * @param {Function} buttonSelectorBuildFn the function to build the jq button selector.
+ * Will be passed $menuElement, parent.
+ * (Defaults to return '#' + $menuElement.attr( 'popupmenu' ); )
+ * @returns {PopupMenu[]} array of popupmenus created
+ */
+ PopupMenu.make_popup_menus = function( parent, menuSelector, buttonSelectorBuildFn ){
+ parent = parent || document;
+ // orig. Glx popupmenu menus have a (non-std) attribute 'popupmenu'
+ // which contains the id of the button that activates the menu
+ menuSelector = menuSelector || 'div[popupmenu]';
+ // default to (orig. Glx) matching button to menu by using the popupmenu attr of the menu as the id of the button
+ buttonSelectorBuildFn = buttonSelectorBuildFn || function( $menuElement, parent ){
+ return '#' + $menuElement.attr( 'popupmenu' );
+ };
+
+ // aggregate and return all PopupMenus
+ var popupMenusCreated = [];
+ $( parent ).find( menuSelector ).each( function(){
+ var $menuElement = $( this ),
+ $buttonElement = $( parent ).find( buttonSelectorBuildFn( $menuElement, parent ) );
+ popupMenusCreated.push( PopupMenu.fromDom( $buttonElement, $menuElement ) );
+ $buttonElement.addClass( 'popup' );
+ });
+ return popupMenusCreated;
+ };
+
+
+ // =============================================================================
+ return PopupMenu;
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! libs/backbone */ 2), __webpack_require__(/*! jquery */ 3), __webpack_require__(/*! underscore */ 1), __webpack_require__(/*! jquery */ 3)))
+
+/***/ },
+/* 63 */
+/*!***************************************************!*\
+ !*** ./galaxy/scripts/mvc/history/copy-dialog.js ***!
+ \***************************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(_, jQuery, $) {!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
+ __webpack_require__(/*! mvc/ui/ui-modal */ 17),
+ __webpack_require__(/*! utils/localization */ 7)
+ ], __WEBPACK_AMD_DEFINE_RESULT__ = function( MODAL, _l ){
+
+ 'use strict';
+
+ //==============================================================================
+ /**
+ * A dialog/modal that allows copying a user history or 'importing' from user
+ * another. Generally called via historyCopyDialog below.
+ * @type {Object}
+ */
+ var CopyDialog = {
+
+ // language related strings/fns
+ defaultName : _.template( "Copy of '<%- name %>'" ),
+ title : _.template( _l( 'Copying history' ) + ' "<%- name %>"' ),
+ submitLabel : _l( 'Copy' ),
+ errorMessage : _l( 'History could not be copied' ),
+ progressive : _l( 'Copying history' ),
+ activeLabel : _l( 'Copy only the active, non-deleted datasets' ),
+ allLabel : _l( 'Copy all datasets including deleted ones' ),
+ anonWarning : _l( 'As an anonymous user, unless you login or register, you will lose your current history ' ) +
+ _l( 'after copying this history. ' ),
+
+ // template for modal body
+ _template : _.template([
+ //TODO: remove inline styles
+ // show a warning message for losing current to anon users
+ '<% if( isAnon ){ %>',
+ '
',
+ '<% } %>',
+ ''
+ ].join( '' )),
+
+ // empty modal body and let the user know the copy is happening
+ _showAjaxIndicator : function _showAjaxIndicator(){
+ var indicator = '
' + this.progressive + '...
';
+ this.modal.$( '.modal-body' ).empty().append( indicator ).css({ 'margin-top': '8px' });
+ },
+
+ // (sorta) public interface - display the modal, render the form, and potentially copy the history
+ // returns a jQuery.Deferred done->history copied, fail->user cancelled
+ dialog : function _dialog( modal, history, options ){
+ options = options || {};
+
+ var dialog = this,
+ deferred = jQuery.Deferred(),
+ // TODO: getting a little byzantine here
+ defaultCopyNameFn = options.nameFn || this.defaultName,
+ defaultCopyName = defaultCopyNameFn({ name: history.get( 'name' ) }),
+ // TODO: these two might be simpler as one 3 state option (all,active,no-choice)
+ defaultCopyWhat = options.allDatasets? 'copy-all' : 'copy-non-deleted',
+ allowAll = !_.isUndefined( options.allowAll )? options.allowAll : true,
+ autoClose = !_.isUndefined( options.autoClose )? options.autoClose : true;
+
+ this.modal = modal;
+
+
+ // validate the name and copy if good
+ function checkNameAndCopy(){
+ var name = modal.$( '#copy-modal-title' ).val();
+ if( !name ){
+ modal.$( '.invalid-title' ).show();
+ return;
+ }
+ // get further settings, shut down and indicate the ajax call, then hide and resolve/reject
+ var copyAllDatasets = modal.$( 'input[name="copy-what"]:checked' ).val() === 'copy-all';
+ modal.$( 'button' ).prop( 'disabled', true );
+ dialog._showAjaxIndicator();
+ history.copy( true, name, copyAllDatasets )
+ .done( function( response ){
+ deferred.resolve( response );
+ })
+ //TODO: make this unneccessary with pub-sub error or handling via Galaxy
+ .fail( function(){
+ alert([ dialog.errorMessage, _l( 'Please contact a Galaxy administrator' ) ].join( '. ' ));
+ deferred.rejectWith( deferred, arguments );
+ })
+ .always( function(){
+ if( autoClose ){ modal.hide(); }
+ });
+ }
+
+ var originalClosingCallback = options.closing_callback;
+ modal.show( _.extend( options, {
+ title : this.title({ name: history.get( 'name' ) }),
+ body : $( dialog._template({
+ name : defaultCopyName,
+ isAnon : Galaxy.user.isAnonymous(),
+ allowAll : allowAll,
+ copyWhat : defaultCopyWhat,
+ activeLabel : this.activeLabel,
+ allLabel : this.allLabel,
+ anonWarning : this.anonWarning,
+ })),
+ buttons : _.object([
+ [ _l( 'Cancel' ), function(){ modal.hide(); } ],
+ [ this.submitLabel, checkNameAndCopy ]
+ ]),
+ height : 'auto',
+ closing_events : true,
+ closing_callback: function _historyCopyClose( cancelled ){
+ if( cancelled ){
+ deferred.reject({ cancelled : true });
+ }
+ if( originalClosingCallback ){
+ originalClosingCallback( cancelled );
+ }
+ }
+ }));
+
+ // set the default dataset copy, autofocus the title, and set up for a simple return
+ modal.$( '#copy-modal-title' ).focus().select();
+ modal.$( '#copy-modal-title' ).on( 'keydown', function( ev ){
+ if( ev.keyCode === 13 ){
+ ev.preventDefault();
+ checkNameAndCopy();
+ }
+ });
+
+ return deferred;
+ },
+ };
+
+ //==============================================================================
+ // maintain the (slight) distinction between copy and import
+ /**
+ * Subclass CopyDialog to use the import language.
+ */
+ var ImportDialog = _.extend( {}, CopyDialog, {
+ defaultName : _.template( "imported: <%- name %>" ),
+ title : _.template( _l( 'Importing history' ) + ' "<%- name %>"' ),
+ submitLabel : _l( 'Import' ),
+ errorMessage : _l( 'History could not be imported' ),
+ progressive : _l( 'Importing history' ),
+ activeLabel : _l( 'Import only the active, non-deleted datasets' ),
+ allLabel : _l( 'Import all datasets including deleted ones' ),
+ anonWarning : _l( 'As an anonymous user, unless you login or register, you will lose your current history ' ) +
+ _l( 'after importing this history. ' ),
+
+ });
+
+ //==============================================================================
+ /**
+ * Main interface for both history import and history copy dialogs.
+ * @param {Backbone.Model} history the history to copy
+ * @param {Object} options a hash
+ * @return {jQuery.Deferred} promise that fails on close and succeeds on copy
+ *
+ * options:
+ * (this object is also passed to the modal used to display the dialog and accepts modal options)
+ * {Function} nameFn if defined, use this to build the default name shown to the user
+ * (the fn is passed: {name: })
+ * {bool} useImport if true, use the 'import' language (instead of Copy)
+ * {bool} allowAll if true, allow the user to choose between copying all datasets and
+ * only non-deleted datasets
+ * {String} allDatasets default initial checked radio button: 'copy-all' or 'copy-non-deleted',
+ */
+ var historyCopyDialog = function( history, options ){
+ options = options || {};
+ // create our own modal if Galaxy doesn't have one (mako tab without use_panels)
+ var modal = window.parent.Galaxy.modal || new MODAL.View({});
+ return options.useImport?
+ ImportDialog.dialog( modal, history, options ):
+ CopyDialog.dialog( modal, history, options );
+ };
+
+
+ //==============================================================================
+ return historyCopyDialog;
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! underscore */ 1), __webpack_require__(/*! jquery */ 3), __webpack_require__(/*! jquery */ 3)))
+
+/***/ },
+/* 64 */
+/*!*****************************************************************!*\
+ !*** ./galaxy/scripts/mvc/history/history-view-edit-current.js ***!
+ \*****************************************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(_, $) {!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
+ __webpack_require__(/*! mvc/history/history-model */ 65),
+ __webpack_require__(/*! mvc/history/history-view-edit */ 75),
+ __webpack_require__(/*! mvc/base-mvc */ 5),
+ __webpack_require__(/*! utils/localization */ 7)
+ ], __WEBPACK_AMD_DEFINE_RESULT__ = function( HISTORY_MODEL, HISTORY_VIEW_EDIT, BASE_MVC, _l ){
+
+ 'use strict';
+
+ // ============================================================================
+ /** session storage for history panel preferences (and to maintain state)
+ */
+ var HistoryViewPrefs = BASE_MVC.SessionStorageModel.extend(
+ /** @lends HistoryViewPrefs.prototype */{
+ defaults : {
+ /** should the tags editor be shown or hidden initially? */
+ tagsEditorShown : false,
+ /** should the annotation editor be shown or hidden initially? */
+ annotationEditorShown : false,
+ ///** what is the currently focused content (dataset or collection) in the current history?
+ // * (the history panel will highlight and scroll to the focused content view)
+ // */
+ //focusedContentId : null
+ /** Current scroll position */
+ scrollPosition : 0
+ },
+ toString : function(){
+ return 'HistoryViewPrefs(' + JSON.stringify( this.toJSON() ) + ')';
+ }
+ });
+
+ /** key string to store panel prefs (made accessible on class so you can access sessionStorage directly) */
+ HistoryViewPrefs.storageKey = function storageKey(){
+ return ( 'history-panel' );
+ };
+
+ /* =============================================================================
+ TODO:
+
+ ============================================================================= */
+ var _super = HISTORY_VIEW_EDIT.HistoryViewEdit;
+ // used in root/index.mako
+ /** @class View/Controller for the user's current history model as used in the history
+ * panel (current right hand panel) of the analysis page.
+ *
+ * The only history panel that:
+ * will poll for updates.
+ * displays datasets in reverse hid order.
+ */
+ var CurrentHistoryView = _super.extend(
+ /** @lends CurrentHistoryView.prototype */{
+
+ /** logger used to record this.log messages, commonly set to console */
+ //logger : console,
+
+ className : _super.prototype.className + ' current-history-panel',
+
+ emptyMsg : _l( "This history is empty. Click 'Get Data' on the left tool menu to start" ),
+ noneFoundMsg : _l( "No matching datasets found" ),
+
+ /** */
+ HDCAViewClass : _super.prototype.HDCAViewClass.extend({
+ foldoutStyle : 'drilldown'
+ }),
+
+ // ......................................................................... SET UP
+ /** Set up the view, set up storage, bind listeners to HistoryContents events */
+ initialize : function( attributes ){
+ attributes = attributes || {};
+
+ // ---- persistent preferences
+ /** maintain state / preferences over page loads */
+ this.preferences = new HistoryViewPrefs( _.extend({
+ id : HistoryViewPrefs.storageKey()
+ }, _.pick( attributes, _.keys( HistoryViewPrefs.prototype.defaults ) )));
+
+ _super.prototype.initialize.call( this, attributes );
+
+ /** sub-views that will overlay this panel (collections) */
+ this.panelStack = [];
+
+ /** id of currently focused content */
+ this.currentContentId = attributes.currentContentId || null;
+ //NOTE: purposely not sent to localstorage since panel recreation roughly lines up with a reset of this value
+ },
+
+ /** Override to cache the current scroll position with a listener */
+ _setUpListeners : function(){
+ _super.prototype._setUpListeners.call( this );
+
+ var panel = this;
+ // reset scroll position when there's a new history
+ this.on( 'new-model', function(){
+ panel.preferences.set( 'scrollPosition', 0 );
+ });
+ },
+
+ // ------------------------------------------------------------------------ loading history/item models
+ // TODO: next three more appropriate moved to the app level
+ /** (re-)loads the user's current history & contents w/ details */
+ loadCurrentHistory : function(){
+ return this.loadHistory( null, { url : Galaxy.root + 'history/current_history_json' });
+ },
+
+ /** loads a history & contents w/ details and makes them the current history */
+ switchToHistory : function( historyId, attributes ){
+ if( Galaxy.user.isAnonymous() ){
+ this.trigger( 'error', _l( 'You must be logged in to switch histories' ) );
+ return $.when();
+ }
+ return this.loadHistory( historyId, { url : Galaxy.root + 'history/set_as_current?id=' + historyId });
+ },
+
+ /** creates a new history on the server and sets it as the user's current history */
+ createNewHistory : function( attributes ){
+ if( Galaxy.user.isAnonymous() ){
+ this.trigger( 'error', _l( 'You must be logged in to create histories' ) );
+ return $.when();
+ }
+ return this.loadHistory( null, { url : Galaxy.root + 'history/create_new_current' });
+ },
+
+ /** release/free/shutdown old models and set up panel for new models */
+ setModel : function( model, attributes, render ){
+ _super.prototype.setModel.call( this, model, attributes, render );
+ if( this.model && this.model.id ){
+ this.log( 'checking for updates' );
+ this.model.checkForUpdates();
+ }
+ return this;
+ },
+
+ // ------------------------------------------------------------------------ history/content event listening
+ /** listening for history events */
+ _setUpModelListeners : function(){
+ _super.prototype._setUpModelListeners.call( this );
+ // re-broadcast any model change events so that listeners don't have to re-bind to each history
+ return this.listenTo( this.model, {
+ 'change:nice_size change:size' : function(){
+ this.trigger( 'history-size-change', this, this.model, arguments );
+ },
+ 'change:id' : function(){
+ this.once( 'loading-done', function(){ this.model.checkForUpdates(); });
+ }
+ });
+ },
+
+ /** listening for collection events */
+ _setUpCollectionListeners : function(){
+ _super.prototype._setUpCollectionListeners.call( this );
+ //TODO:?? may not be needed? see history-view-edit, 369
+ // if a hidden item is created (gen. by a workflow), moves thru the updater to the ready state,
+ // then: remove it from the collection if the panel is set to NOT show hidden datasets
+ this.listenTo( this.collection, 'state:ready', function( model, newState, oldState ){
+ if( ( !model.get( 'visible' ) )
+ && ( !this.storage.get( 'show_hidden' ) ) ){
+ this.removeItemView( model );
+ }
+ });
+ },
+
+ // ------------------------------------------------------------------------ panel rendering
+ /** override to add a handler to capture the scroll position when the parent scrolls */
+ _setUpBehaviors : function( $where ){
+ $where = $where || this.$el;
+ // we need to call this in _setUpBehaviors which is called after render since the $el
+ // may not be attached to $el.parent and $scrollContainer() may not work
+ var panel = this;
+ _super.prototype._setUpBehaviors.call( panel, $where );
+
+ // cache the handler to remove and re-add so we don't pile up the handlers
+ if( !this._debouncedScrollCaptureHandler ){
+ this._debouncedScrollCaptureHandler = _.debounce( function scrollCapture(){
+ // cache the scroll position (only if visible)
+ if( panel.$el.is( ':visible' ) ){
+ panel.preferences.set( 'scrollPosition', $( this ).scrollTop() );
+ }
+ }, 40 );
+ }
+
+ panel.$scrollContainer()
+ .off( 'scroll', this._debouncedScrollCaptureHandler )
+ .on( 'scroll', this._debouncedScrollCaptureHandler );
+ return panel;
+ },
+
+ /** In this override, handle null models and move the search input to the top */
+ _buildNewRender : function(){
+ if( !this.model ){ return $(); }
+ var $newRender = _super.prototype._buildNewRender.call( this );
+ //TODO: hacky
+ $newRender.find( '.search' ).prependTo( $newRender.find( '.controls' ) );
+ this._renderQuotaMessage( $newRender );
+ return $newRender;
+ },
+
+ /** render the message displayed when a user is over quota and can't run jobs */
+ _renderQuotaMessage : function( $whereTo ){
+ $whereTo = $whereTo || this.$el;
+ return $( this.templates.quotaMsg( {}, this ) ).prependTo( $whereTo.find( '.messages' ) );
+ },
+
+ /** In this override, add links to open data uploader or get data in the tools section */
+ _renderEmptyMessage : function( $whereTo ){
+ var panel = this,
+ $emptyMsg = panel.$emptyMessage( $whereTo ),
+ $toolMenu = $( '.toolMenuContainer' );
+
+ if( ( _.isEmpty( panel.views ) && !panel.searchFor )
+ && ( Galaxy && Galaxy.upload && $toolMenu.size() ) ){
+ $emptyMsg.empty();
+
+ $emptyMsg.html([
+ _l( 'This history is empty' ), '. ', _l( 'You can ' ),
+ '',
+ _l( 'load your own data' ),
+ '',
+ _l( ' or ' ), '',
+ _l( 'get data from an external source' ),
+ ''
+ ].join('') );
+ $emptyMsg.find( '.uploader-link' ).click( function( ev ){
+ Galaxy.upload.show( ev );
+ });
+ $emptyMsg.find( '.get-data-link' ).click( function( ev ){
+ $toolMenu.parent().scrollTop( 0 );
+ $toolMenu.find( 'span:contains("Get Data")' ).click();
+ });
+ return $emptyMsg.show();
+ }
+ return _super.prototype._renderEmptyMessage.call( this, $whereTo );
+ },
+
+ /** In this override, get and set current panel preferences when editor is used */
+ _renderTags : function( $where ){
+ var panel = this;
+ // render tags and show/hide based on preferences
+ _super.prototype._renderTags.call( panel, $where );
+ if( panel.preferences.get( 'tagsEditorShown' ) ){
+ panel.tagsEditor.toggle( true );
+ }
+ // store preference when shown or hidden
+ panel.listenTo( panel.tagsEditor, 'hiddenUntilActivated:shown hiddenUntilActivated:hidden',
+ function( tagsEditor ){
+ panel.preferences.set( 'tagsEditorShown', tagsEditor.hidden );
+ }
+ );
+ },
+
+ /** In this override, get and set current panel preferences when editor is used */
+ _renderAnnotation : function( $where ){
+ var panel = this;
+ // render annotation and show/hide based on preferences
+ _super.prototype._renderAnnotation.call( panel, $where );
+ if( panel.preferences.get( 'annotationEditorShown' ) ){
+ panel.annotationEditor.toggle( true );
+ }
+ // store preference when shown or hidden
+ panel.listenTo( panel.annotationEditor, 'hiddenUntilActivated:shown hiddenUntilActivated:hidden',
+ function( annotationEditor ){
+ panel.preferences.set( 'annotationEditorShown', annotationEditor.hidden );
+ }
+ );
+ },
+
+ /** Override to scroll to cached position (in prefs) after swapping */
+ _swapNewRender : function( $newRender ){
+ _super.prototype._swapNewRender.call( this, $newRender );
+ var panel = this;
+ _.delay( function(){
+ var pos = panel.preferences.get( 'scrollPosition' );
+ if( pos ){
+ panel.scrollTo( pos, 0 );
+ }
+ }, 10 );
+ //TODO: is this enough of a delay on larger histories?
+
+ return this;
+ },
+
+ // ------------------------------------------------------------------------ sub-views
+ /** Override to add the current-content highlight class to currentContentId's view */
+ _attachItems : function( $whereTo ){
+ _super.prototype._attachItems.call( this, $whereTo );
+ var panel = this;
+ if( panel.currentContentId ){
+ panel._setCurrentContentById( panel.currentContentId );
+ }
+ return this;
+ },
+
+ /** Override to remove any drill down panels */
+ addItemView : function( model, collection, options ){
+ var view = _super.prototype.addItemView.call( this, model, collection, options );
+ if( !view ){ return view; }
+ if( this.panelStack.length ){ return this._collapseDrilldownPanel(); }
+ return view;
+ },
+
+ // ------------------------------------------------------------------------ collection sub-views
+ /** In this override, add/remove expanded/collapsed model ids to/from web storage */
+ _setUpItemViewListeners : function( view ){
+ var panel = this;
+ _super.prototype._setUpItemViewListeners.call( panel, view );
+ // use pub-sub to: handle drilldown expansion and collapse
+ return panel.listenTo( view, {
+ 'expanded:drilldown' : function( v, drilldown ){
+ this._expandDrilldownPanel( drilldown );
+ },
+ 'collapsed:drilldown' : function( v, drilldown ){
+ this._collapseDrilldownPanel( drilldown );
+ },
+ });
+ },
+
+ /** display 'current content': add a visible highlight and store the id of a content item */
+ setCurrentContent : function( view ){
+ this.$( '.history-content.current-content' ).removeClass( 'current-content' );
+ if( view ){
+ view.$el.addClass( 'current-content' );
+ this.currentContentId = view.model.id;
+ } else {
+ this.currentContentId = null;
+ }
+ },
+
+ /** find the view with the id and then call setCurrentContent on it */
+ _setCurrentContentById : function( id ){
+ var view = this.viewFromModelId( id ) || null;
+ this.setCurrentContent( view );
+ },
+
+ /** Handle drill down by hiding this panels list and controls and showing the sub-panel */
+ _expandDrilldownPanel : function( drilldown ){
+ this.panelStack.push( drilldown );
+ // hide this panel's controls and list, set the name for back navigation, and attach to the $el
+ this.$( '> .controls' ).add( this.$list() ).hide();
+ drilldown.parentName = this.model.get( 'name' );
+ this.$el.append( drilldown.render().$el );
+ },
+
+ /** Handle drilldown close by freeing the panel and re-rendering this panel */
+ _collapseDrilldownPanel : function( drilldown ){
+ this.panelStack.pop();
+ //TODO: MEM: free the panel
+ this.render();
+ },
+
+ // ........................................................................ external objects/MVC
+ listenToGalaxy : function( galaxy ){
+ // TODO: MEM: questionable reference island / closure practice
+ this.listenTo( galaxy, 'galaxy_main:load', function( data ){
+ var pathToMatch = data.fullpath,
+ useToURLRegexMap = {
+ 'display' : /datasets\/([a-f0-9]+)\/display/,
+ 'edit' : /datasets\/([a-f0-9]+)\/edit/,
+ 'report_error' : /dataset\/errors\?id=([a-f0-9]+)/,
+ 'rerun' : /tool_runner\/rerun\?id=([a-f0-9]+)/,
+ 'show_params' : /datasets\/([a-f0-9]+)\/show_params/,
+ // no great way to do this here? (leave it in the dataset event handlers above?)
+ // 'visualization' : 'visualization',
+ },
+ hdaId = null,
+ hdaUse = null;
+ _.find( useToURLRegexMap, function( regex, use ){
+ var match = pathToMatch.match( regex );
+ if( match && match.length == 2 ){
+ hdaId = match[1];
+ hdaUse = use;
+ return true;
+ }
+ return false;
+ });
+ // need to type mangle to go from web route to history contents
+ hdaId = 'dataset-' + hdaId;
+ this._setCurrentContentById( hdaId );
+ });
+ },
+
+ //TODO: remove quota meter from panel and remove this
+ /** add listeners to an external quota meter (mvc/user/user-quotameter.js) */
+ connectToQuotaMeter : function( quotaMeter ){
+ if( !quotaMeter ){
+ return this;
+ }
+ // show/hide the 'over quota message' in the history when the meter tells it to
+ this.listenTo( quotaMeter, 'quota:over', this.showQuotaMessage );
+ this.listenTo( quotaMeter, 'quota:under', this.hideQuotaMessage );
+
+ // having to add this to handle re-render of hview while overquota (the above do not fire)
+ this.on( 'rendered rendered:initial', function(){
+ if( quotaMeter && quotaMeter.isOverQuota() ){
+ this.showQuotaMessage();
+ }
+ });
+ return this;
+ },
+
+ //TODO: this seems more like a per user message than a history message; IOW, this doesn't belong here
+ /** Override to preserve the quota message */
+ clearMessages : function( ev ){
+ var $target = !_.isUndefined( ev )?
+ $( ev.currentTarget )
+ :this.$messages().children( '[class$="message"]' );
+ $target = $target.not( '.quota-message' );
+ $target.fadeOut( this.fxSpeed, function(){
+ $( this ).remove();
+ });
+ return this;
+ },
+
+ /** Show the over quota message (which happens to be in the history panel).
+ */
+ showQuotaMessage : function(){
+ var $msg = this.$( '.quota-message' );
+ if( $msg.is( ':hidden' ) ){ $msg.slideDown( this.fxSpeed ); }
+ },
+
+ //TODO: this seems more like a per user message than a history message
+ /** Hide the over quota message (which happens to be in the history panel).
+ */
+ hideQuotaMessage : function(){
+ var $msg = this.$( '.quota-message' );
+ if( !$msg.is( ':hidden' ) ){ $msg.slideUp( this.fxSpeed ); }
+ },
+
+ /** Return a string rep of the history
+ */
+ toString : function(){
+ return 'CurrentHistoryView(' + (( this.model )?( this.model.get( 'name' )):( '' )) + ')';
+ }
+ });
+
+
+ //------------------------------------------------------------------------------ TEMPLATES
+ CurrentHistoryView.prototype.templates = (function(){
+
+ var quotaMsgTemplate = BASE_MVC.wrapTemplate([
+ '
',
+ _l( 'You are over your disk quota' ), '. ',
+ _l( 'Tool execution is on hold until your disk usage drops below your allocated quota' ), '.',
+ '
'
+ ], 'history' );
+ return _.extend( _.clone( _super.prototype.templates ), {
+ quotaMsg : quotaMsgTemplate
+ });
+
+ }());
+
+
+ //==============================================================================
+ return {
+ CurrentHistoryView : CurrentHistoryView
+ };
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! underscore */ 1), __webpack_require__(/*! jquery */ 3)))
+
+/***/ },
+/* 65 */
+/*!*****************************************************!*\
+ !*** ./galaxy/scripts/mvc/history/history-model.js ***!
+ \*****************************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(Backbone, jQuery, _, $) {
+ !(__WEBPACK_AMD_DEFINE_ARRAY__ = [
+ __webpack_require__(/*! mvc/history/history-contents */ 66),
+ __webpack_require__(/*! mvc/history/history-preferences */ 74),
+ __webpack_require__(/*! mvc/base/controlled-fetch-collection */ 67),
+ __webpack_require__(/*! utils/utils */ 21),
+ __webpack_require__(/*! mvc/base-mvc */ 5),
+ __webpack_require__(/*! utils/localization */ 7)
+ ], __WEBPACK_AMD_DEFINE_RESULT__ = function( HISTORY_CONTENTS, HISTORY_PREFS, CONTROLLED_FETCH_COLLECTION, UTILS, BASE_MVC, _l ){
+ 'use strict';
+
+ //==============================================================================
+ /** @class Model for a Galaxy history resource - both a record of user
+ * tool use and a collection of the datasets those tools produced.
+ * @name History
+ * @augments Backbone.Model
+ */
+ var History = Backbone.Model
+ .extend( BASE_MVC.LoggableMixin )
+ .extend( BASE_MVC.mixin( BASE_MVC.SearchableModelMixin, /** @lends History.prototype */{
+ _logNamespace : 'history',
+
+ /** ms between fetches when checking running jobs/datasets for updates */
+ UPDATE_DELAY : 4000,
+
+ // values from api (may need more)
+ defaults : {
+ model_class : 'History',
+ id : null,
+ name : 'Unnamed History',
+ state : 'new',
+
+ deleted : false,
+ contents_shown : {},
+ },
+
+ urlRoot: Galaxy.root + 'api/histories',
+
+ // ........................................................................ set up/tear down
+ /** Set up the model
+ * @param {Object} historyJSON model data for this History
+ * @param {Object[]} contentsJSON array of model data for this History's contents (hdas or collections)
+ * @param {Object} options any extra settings including logger
+ */
+ initialize : function( historyJSON, contentsJSON, options ){
+ options = options || {};
+ this.logger = options.logger || null;
+ this.log( this + ".initialize:", historyJSON, contentsJSON, options );
+
+ /** HistoryContents collection of the HDAs contained in this history. */
+ this.contents = new HISTORY_CONTENTS.HistoryContents( contentsJSON || [], {
+ historyId : this.get( 'id' ),
+ order : options.order,
+ });
+
+ this._setUpListeners();
+ this._setUpCollectionListeners();
+
+ /** cached timeout id for the dataset updater */
+ this.updateTimeoutId = null;
+ },
+
+ /** set up any event listeners for this history including those to the contained HDAs
+ * events: error:contents if an error occurred with the contents collection
+ */
+ _setUpListeners : function(){
+ // if the model's id changes ('current' or null -> an actual id), update the contents history_id
+ return this.on({
+ 'error' : function( model, xhr, options, msg, details ){
+ this.clearUpdateTimeout();
+ },
+ 'change:id' : function( model, newId ){
+ if( this.contents ){
+ this.contents.historyId = newId;
+ }
+ },
+ });
+ },
+
+ /** */
+ _setUpCollectionListeners : function(){
+ if( !this.contents ){ return this; }
+ // bubble up errors
+ return this.listenTo( this.contents, {
+ 'error' : function(){
+ this.trigger.apply( this, jQuery.makeArray( arguments ) );
+ },
+ });
+ },
+
+ // ........................................................................ derived attributes
+ /** convert size in bytes to a more human readable version */
+ nice_size : function(){
+ return UTILS.bytesToString( this.get( 'size' ), true, 2 );
+ },
+
+ /** override to add nice_size */
+ toJSON : function(){
+ return _.extend( Backbone.Model.prototype.toJSON.call( this ), {
+ nice_size : this.nice_size()
+ });
+ },
+
+ /** override to allow getting nice_size */
+ get : function( key ){
+ if( key === 'nice_size' ){
+ return this.nice_size();
+ }
+ return Backbone.Model.prototype.get.apply( this, arguments );
+ },
+
+ // ........................................................................ common queries
+ /** T/F is this history owned by the current user (Galaxy.user)
+ * Note: that this will return false for an anon user even if the history is theirs.
+ */
+ ownedByCurrUser : function(){
+ // no currUser
+ if( !Galaxy || !Galaxy.user ){
+ return false;
+ }
+ // user is anon or history isn't owned
+ if( Galaxy.user.isAnonymous() || Galaxy.user.id !== this.get( 'user_id' ) ){
+ return false;
+ }
+ return true;
+ },
+
+ /** Return the number of running jobs assoc with this history (note: unknown === 0) */
+ numOfUnfinishedJobs : function(){
+ var unfinishedJobIds = this.get( 'non_ready_jobs' );
+ return unfinishedJobIds? unfinishedJobIds.length : 0;
+ },
+
+ /** Return the number of running hda/hdcas in this history (note: unknown === 0) */
+ numOfUnfinishedShownContents : function(){
+ var contents = this.contents.running().visibleAndUndeleted();
+ return contents? contents.length : 0;
+ },
+
+ // ........................................................................ search
+ /** What model fields to search with */
+ searchAttributes : [
+ 'name', 'annotation', 'tags'
+ ],
+
+ /** Adding title and singular tag */
+ searchAliases : {
+ title : 'name',
+ tag : 'tags'
+ },
+
+ // ........................................................................ updates
+ _getSizeAndRunning : function(){
+ return this.fetch({ data : $.param({ keys : 'size,non_ready_jobs' }) });
+ },
+
+ /** */
+ refresh : function( options ){
+ // console.log( this + '.refresh' );
+ options = options || {};
+ var self = this;
+
+ // note if there was no previous update time, all summary contents will be fetched
+ var lastUpdateTime = self.lastUpdateTime;
+ self.lastUpdateTime = new Date();
+
+ // if we don't flip this, then a fully-fetched list will not be re-checked via fetch
+ this.contents.allFetched = false;
+ return self.contents.fetchUpdated( lastUpdateTime )
+ .done( _.bind( self.checkForUpdates, self ) );
+ },
+
+ /** */
+ checkForUpdates : function( options ){
+ // console.log( this + '.checkForUpdates' );
+ options = options || {};
+ var delay = this.UPDATE_DELAY;
+ var self = this;
+ if( !self.id ){ return; }
+
+ function _delayThenUpdate(){
+ // prevent buildup of updater timeouts by clearing previous if any, then set new and cache id
+ self.clearUpdateTimeout();
+ self.updateTimeoutId = setTimeout( function(){
+ self.refresh( options );
+ }, delay );
+ }
+
+ // if there are still datasets in the non-ready state, recurse into this function with the new time
+ var nonReadyContentCount = this.numOfUnfinishedShownContents();
+ // console.log( 'nonReadyContentCount:', nonReadyContentCount );
+ if( nonReadyContentCount > 0 ){
+ _delayThenUpdate();
+
+ } else {
+ // no datasets are running, but currently runnning jobs may still produce new datasets
+ // see if the history has any running jobs and continue to update if so
+ // (also update the size for the user in either case)
+ self._getSizeAndRunning()
+ .done( function( historyData ){
+ // console.log( 'non_ready_jobs:', historyData.non_ready_jobs );
+ if( self.numOfUnfinishedJobs() > 0 ){
+ _delayThenUpdate();
+
+ } else {
+ // otherwise, let listeners know that all updates have stopped
+ self.trigger( 'ready' );
+ // self.lastUpdateTime = null;
+ }
+ });
+ }
+ },
+
+ /** clear the timeout and the cached timeout id */
+ clearUpdateTimeout : function(){
+ if( this.updateTimeoutId ){
+ clearTimeout( this.updateTimeoutId );
+ this.updateTimeoutId = null;
+ }
+ },
+
+ // ........................................................................ ajax
+ /** override to use actual Dates objects for create/update times */
+ parse : function( response, options ){
+ var parsed = Backbone.Model.prototype.parse.call( this, response, options );
+ if( parsed.create_time ){
+ parsed.create_time = new Date( parsed.create_time );
+ }
+ if( parsed.update_time ){
+ parsed.update_time = new Date( parsed.update_time );
+ }
+ return parsed;
+ },
+
+ /** */
+ fetchWithContents : function( options, contentsOptions ){
+ options = options || {};
+ var self = this;
+
+ // console.log( this + '.fetchWithContents' );
+ // TODO: push down to a base class
+ options.view = 'current';
+ // fetch history then use history data to fetch (paginated) contents
+ return this.fetch( options ).pipe( function getContents( history ){
+ self.contents.historyId = history.id;
+ return self.fetchContents( contentsOptions );
+ });
+ },
+
+ /** */
+ fetchContents : function( options ){
+ options = options || {};
+ var self = this;
+ // now that we have a history id, we can read the prefs
+ // and make the contents fetch based on those
+ var prefs = HISTORY_PREFS.HistoryPrefs.get( self.id ).toJSON();
+ self.contents.includeDeleted = prefs.show_deleted;
+ self.contents.includeHidden = prefs.show_hidden;
+
+ // we're updating, reset the update time
+ self.lastUpdateTime = new Date();
+ return self.contents.fetchFirst( options );
+ },
+
+ /** save this history, _Mark_ing it as deleted (just a flag) */
+ _delete : function( options ){
+ if( this.get( 'deleted' ) ){ return jQuery.when(); }
+ return this.save( { deleted: true }, options );
+ },
+ /** purge this history, _Mark_ing it as purged and removing all dataset data from the server */
+ purge : function( options ){
+ if( this.get( 'purged' ) ){ return jQuery.when(); }
+ return this.save( { deleted: true, purged: true }, options );
+ },
+ /** save this history, _Mark_ing it as undeleted */
+ undelete : function( options ){
+ if( !this.get( 'deleted' ) ){ return jQuery.when(); }
+ return this.save( { deleted: false }, options );
+ },
+
+ /** Make a copy of this history on the server
+ * @param {Boolean} current if true, set the copy as the new current history (default: true)
+ * @param {String} name name of new history (default: none - server sets to: Copy of )
+ * @fires copied passed this history and the response JSON from the copy
+ * @returns {xhr}
+ */
+ copy : function( current, name, allDatasets ){
+ current = ( current !== undefined )?( current ):( true );
+ if( !this.id ){
+ throw new Error( 'You must set the history ID before copying it.' );
+ }
+
+ var postData = { history_id : this.id };
+ if( current ){
+ postData.current = true;
+ }
+ if( name ){
+ postData.name = name;
+ }
+ if( !allDatasets ){
+ postData.all_datasets = false;
+ }
+ postData.view = 'current';
+
+ var history = this;
+ var copy = jQuery.post( this.urlRoot, postData );
+ // if current - queue to setAsCurrent before firing 'copied'
+ if( current ){
+ return copy.then( function( response ){
+ var newHistory = new History( response );
+ return newHistory.setAsCurrent()
+ .done( function(){
+ history.trigger( 'copied', history, response );
+ });
+ });
+ }
+ return copy.done( function( response ){
+ history.trigger( 'copied', history, response );
+ });
+ },
+
+ setAsCurrent : function(){
+ var history = this,
+ xhr = jQuery.getJSON( Galaxy.root + 'history/set_as_current?id=' + this.id );
+
+ xhr.done( function(){
+ history.trigger( 'set-as-current', history );
+ });
+ return xhr;
+ },
+
+ // ........................................................................ misc
+ toString : function(){
+ return 'History(' + this.get( 'id' ) + ',' + this.get( 'name' ) + ')';
+ }
+ }));
+
+
+ //==============================================================================
+ var _collectionSuper = CONTROLLED_FETCH_COLLECTION.PaginatedCollection;
+ /** @class A collection of histories (per user)
+ * that maintains the current history as the first in the collection.
+ * New or copied histories become the current history.
+ */
+ var HistoryCollection = _collectionSuper.extend( BASE_MVC.LoggableMixin ).extend({
+ _logNamespace : 'history',
+
+ model : History,
+ /** @type {String} initial order used by collection */
+ order : 'update_time',
+ /** @type {Number} limit used for the first fetch (or a reset) */
+ limitOnFirstFetch : 10,
+ /** @type {Number} limit used for each subsequent fetch */
+ limitPerFetch : 10,
+
+ initialize : function( models, options ){
+ options = options || {};
+ this.log( 'HistoryCollection.initialize', models, options );
+ _collectionSuper.prototype.initialize.call( this, models, options );
+
+ /** @type {boolean} should deleted histories be included */
+ this.includeDeleted = options.includeDeleted || false;
+
+ /** @type {String} encoded id of the history that's current */
+ this.currentHistoryId = options.currentHistoryId;
+
+ this.setUpListeners();
+ // note: models are sent to reset *after* this fn ends; up to this point
+ // the collection *is empty*
+ },
+
+ urlRoot : Galaxy.root + 'api/histories',
+
+ url : function(){ return this.urlRoot; },
+
+ /** set up reflexive event handlers */
+ setUpListeners : function setUpListeners(){
+ return this.on({
+ // when a history is deleted, remove it from the collection (if optionally set to do so)
+ 'change:deleted' : function( history ){
+ // TODO: this becomes complicated when more filters are used
+ this.debug( 'change:deleted', this.includeDeleted, history.get( 'deleted' ) );
+ if( !this.includeDeleted && history.get( 'deleted' ) ){
+ this.remove( history );
+ }
+ },
+ // listen for a history copy, setting it to current
+ 'copied' : function( original, newData ){
+ this.setCurrent( new History( newData, [] ) );
+ },
+ // when a history is made current, track the id in the collection
+ 'set-as-current' : function( history ){
+ var oldCurrentId = this.currentHistoryId;
+ this.trigger( 'no-longer-current', oldCurrentId );
+ this.currentHistoryId = history.id;
+ //TODO:? sort?
+ }
+ });
+ },
+
+ /** override to change view */
+ _buildFetchData : function( options ){
+ return _.extend( _collectionSuper.prototype._buildFetchData.call( this, options ), {
+ view : 'current'
+ });
+ },
+
+ /** override to filter out deleted and purged */
+ _buildFetchFilters : function( options ){
+ var superFilters = _collectionSuper.prototype._buildFetchFilters.call( this, options ) || {};
+ var filters = {};
+ if( !this.includeDeleted ){
+ filters.deleted = false;
+ filters.purged = false;
+ } else {
+ // force API to return both deleted and non
+ //TODO: when the API is updated, remove this
+ filters.deleted = null;
+ }
+ return _.defaults( superFilters, filters );
+ },
+
+ /** @type {Object} map of collection available sorting orders containing comparator fns */
+ comparators : _.extend( _.clone( _collectionSuper.prototype.comparators ), {
+ 'name' : BASE_MVC.buildComparator( 'name', { ascending: true }),
+ 'name-dsc' : BASE_MVC.buildComparator( 'name', { ascending: false }),
+ 'size' : BASE_MVC.buildComparator( 'size', { ascending: false }),
+ 'size-asc' : BASE_MVC.buildComparator( 'size', { ascending: true }),
+ }),
+
+ /** override to always have the current history first */
+ sort : function( options ){
+ options = options || {};
+ var silent = options.silent;
+ var currentHistory = this.remove( this.get( this.currentHistoryId ) );
+ _collectionSuper.prototype.sort.call( this, _.defaults({ silent: true }, options ) );
+ this.unshift( currentHistory, { silent: true });
+ if( !silent ){
+ this.trigger( 'sort', this, options );
+ }
+ return this;
+ },
+
+ /** create a new history and by default set it to be the current history */
+ create : function create( data, hdas, historyOptions, xhrOptions ){
+ //TODO: .create is actually a collection function that's overridden here
+ var collection = this,
+ xhr = jQuery.getJSON( Galaxy.root + 'history/create_new_current' );
+ return xhr.done( function( newData ){
+ collection.setCurrent( new History( newData, [], historyOptions || {} ) );
+ });
+ },
+
+ /** set the current history to the given history, placing it first in the collection.
+ * Pass standard bbone options for use in unshift.
+ * @triggers new-current passed history and this collection
+ */
+ setCurrent : function( history, options ){
+ options = options || {};
+ // new histories go in the front
+ this.unshift( history, options );
+ this.currentHistoryId = history.get( 'id' );
+ if( !options.silent ){
+ this.trigger( 'new-current', history, this );
+ }
+ return this;
+ },
+
+ toString: function toString(){
+ return 'HistoryCollection(' + this.length + ',current:' + this.currentHistoryId + ')';
+ }
+ });
+ window.HistoryCollection = HistoryCollection;
+
+
+ //==============================================================================
+ return {
+ History : History,
+ HistoryCollection : HistoryCollection
+ };}.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! libs/backbone */ 2), __webpack_require__(/*! jquery */ 3), __webpack_require__(/*! underscore */ 1), __webpack_require__(/*! jquery */ 3)))
+
+/***/ },
+/* 66 */
+/*!********************************************************!*\
+ !*** ./galaxy/scripts/mvc/history/history-contents.js ***!
+ \********************************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(_, jQuery, Backbone) {!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
+ __webpack_require__(/*! mvc/base/controlled-fetch-collection */ 67),
+ __webpack_require__(/*! mvc/history/hda-model */ 68),
+ __webpack_require__(/*! mvc/history/hdca-model */ 72),
+ __webpack_require__(/*! mvc/history/history-preferences */ 74),
+ __webpack_require__(/*! mvc/base-mvc */ 5)
+ ], __WEBPACK_AMD_DEFINE_RESULT__ = function( CONTROLLED_FETCH_COLLECTION, HDA_MODEL, HDCA_MODEL, HISTORY_PREFS, BASE_MVC ){
+ 'use strict';
+
+ //==============================================================================
+ var _super = CONTROLLED_FETCH_COLLECTION.PaginatedCollection;
+ /** @class Backbone collection for history content.
+ * NOTE: history content seems like a dataset collection, but differs in that it is mixed:
+ * each element can be either an HDA (dataset) or a DatasetCollection and co-exist on
+ * the same level.
+ * Dataset collections on the other hand are not mixed and (so far) can only contain either
+ * HDAs or child dataset collections on one level.
+ * This is why this does not inherit from any of the DatasetCollections (currently).
+ */
+ var HistoryContents = _super.extend( BASE_MVC.LoggableMixin ).extend({
+ _logNamespace : 'history',
+
+ /** since history content is a mix, override model fn into a factory, creating based on history_content_type */
+ model : function( attrs, options ) {
+ if( attrs.history_content_type === "dataset" ) {
+ return new HDA_MODEL.HistoryDatasetAssociation( attrs, options );
+
+ } else if( attrs.history_content_type === "dataset_collection" ) {
+ switch( attrs.collection_type ){
+ case 'list':
+ return new HDCA_MODEL.HistoryListDatasetCollection( attrs, options );
+ case 'paired':
+ return new HDCA_MODEL.HistoryPairDatasetCollection( attrs, options );
+ case 'list:paired':
+ return new HDCA_MODEL.HistoryListPairedDatasetCollection( attrs, options );
+ }
+ // This is a hack inside a hack:
+ // Raise a plain object with validationError to fake a model.validationError
+ // (since we don't have a model to use validate with)
+ // (the outer hack being the mixed content/model function in this collection)
+ return { validationError : 'Unknown collection_type: ' + attrs.history_content_type };
+ }
+ return { validationError : 'Unknown history_content_type: ' + attrs.history_content_type };
+ },
+
+ /** @type {Number} limit used for the first fetch (or a reset) */
+ limitOnFirstFetch : 100,
+ /** @type {Number} limit used for each subsequent fetch */
+ limitPerFetch : 50,
+
+ /** @type {String} order used here and when fetching from server */
+ order : 'create_time',
+
+ /** Set up */
+ initialize : function( models, options ){
+ options = options || {};
+ this.historyId = options.historyId || null;
+
+ // backbonejs uses collection.model.prototype.idAttribute to determine if a model is *already* in a collection
+ // and either merged or replaced. In this case, our 'model' is a function so we need to add idAttribute
+ // manually here - if we don't, contents will not merge but be replaced/swapped.
+ this.model.prototype.idAttribute = 'type_id';
+
+ /** @type {Boolean} does this collection contain and fetch deleted elements */
+ this.includeDeleted = options.includeDeleted || false;
+ /** @type {Boolean} does this collection contain and fetch non-visible elements */
+ this.includeHidden = options.includeHidden || false;
+
+ return _super.prototype.initialize.call( this, models, options );
+ },
+
+ /** root api url */
+ urlRoot : Galaxy.root + 'api/histories',
+
+ /** complete api url */
+ url : function(){
+ return this.urlRoot + '/' + this.historyId + '/contents';
+ },
+
+ // ........................................................................ common queries
+ /** Get the id of every model in this collection not in a 'ready' state (running).
+ * @returns an array of model ids
+ * @see HistoryDatasetAssociation#inReadyState
+ */
+ running : function(){
+ function filterFn( c ){ return !c.inReadyState(); }
+ return new HistoryContents( this.filter( filterFn ) );
+ },
+
+ /** Get the model with the given hid
+ * @param {Int} hid the hid to search for
+ * @returns {HistoryDatasetAssociation} the model with the given hid or undefined if not found
+ */
+ getByHid : function( hid ){
+ return _.first( this.filter( function( content ){ return content.get( 'hid' ) === hid; }) );
+ },
+
+ /** return true if all contents have details */
+ haveDetails : function(){
+ return this.all( function( content ){ return content.hasDetails(); });
+ },
+
+ // .................... hidden / deleted
+ /** return a new contents collection of only hidden items */
+ hidden : function(){
+ function filterFn( c ){ return c.hidden(); }
+ return new HistoryContents( this.filter( filterFn ) );
+ },
+
+ /** return a new contents collection of only hidden items */
+ deleted : function(){
+ function filterFn( c ){ return c.get( 'deleted' ); }
+ return new HistoryContents( this.filter( filterFn ) );
+ },
+
+ /** return a new contents collection of only hidden items */
+ visibleAndUndeleted : function(){
+ function filterFn( c ){ return c.get( 'visible' ) && !c.get( 'deleted' ); }
+ return new HistoryContents( this.filter( filterFn ) );
+ },
+
+ //TODO: revisit includeDeleted/Hidden naming and toggle fn
+ /** create a setter in order to publish the change */
+ setIncludeDeleted : function( setting ){
+ if( _.isBoolean( setting ) && setting !== this.includeDeleted ){
+ this.includeDeleted = setting;
+ this.trigger( 'change:include-deleted', setting, this );
+ }
+ },
+
+ /** create a setter in order to publish the change */
+ setIncludeHidden : function( setting ){
+ if( _.isBoolean( setting ) && setting !== this.includeHidden ){
+ this.includeHidden = setting;
+ this.trigger( 'change:include-hidden', setting, this );
+ }
+ },
+
+ // ........................................................................ ajax
+ /** override to get expanded ids from sessionStorage and pass to API as details */
+ fetch : function( options ){
+ options = options || {};
+ if( this.historyId && !options.details ){
+ var prefs = HISTORY_PREFS.HistoryPrefs.get( this.historyId ).toJSON();
+ options.details = _.values( prefs.expandedIds ).join( ',' );
+ }
+ return _super.prototype.fetch.call( this, options );
+ },
+
+ // ............. ControlledFetch stuff
+ /** */
+ _buildFetchData : function( options ){
+ return _.extend( _super.prototype._buildFetchData.call( this, options ), {
+ v : 'dev'
+ });
+ },
+
+ /** Extend to include details and version */
+ _fetchParams : _super.prototype._fetchParams.concat([
+ // TODO: remove (the need for) both
+ /** version */
+ 'v',
+ /** dataset ids to get full details of */
+ 'details',
+ ]),
+
+ /** */
+ _buildFetchFilters : function( options ){
+ var superFilters = _super.prototype._buildFetchFilters.call( this, options ) || {};
+ var filters = {};
+ if( !this.includeDeleted ){
+ filters.deleted = false;
+ filters.purged = false;
+ }
+ if( !this.includeHidden ){
+ filters.visible = true;
+ }
+ return _.defaults( superFilters, filters );
+ },
+
+ /** override to filter requested contents to those updated after the Date 'since' */
+ fetchUpdated : function( since, options ){
+ if( since ){
+ options = options || { filters: {} };
+ options.filters = {
+ 'update_time-ge' : since.toISOString(),
+ // workflows will produce hidden datasets (non-output datasets) that still
+ // need to be updated in the collection or they'll update forever
+ // we can remove the default visible filter by using an 'empty' value
+ visible : ''
+ };
+ }
+ // console.log( 'fetching updated:', this.historyId, since );
+ return this.fetch( options )
+ // .done( function( r ){ console.log( 'updated:\n', JSON.stringify( r ) ); })
+ .done( function( r ){ console.log( 'updated:', r.length, r ); })
+ ;
+ },
+
+ /** */
+ fetchDeleted : function( options ){
+ options = options || {};
+ var self = this;
+ options.filters = _.extend( options.filters, {
+ // all deleted, purged or not
+ deleted : true,
+ purged : undefined
+ });
+
+ self.trigger( 'fetching-deleted', self );
+ return self.fetch( options )
+ .always( function(){ self.trigger( 'fetching-deleted-done', self ); });
+ },
+
+ /** */
+ fetchHidden : function( options ){
+ options = options || {};
+ var self = this;
+ options.filters = _.extend( options.filters, {
+ visible : false
+ });
+
+ self.trigger( 'fetching-hidden', self );
+ return self.fetch( options )
+ .always( function(){ self.trigger( 'fetching-hidden-done', self ); });
+ },
+
+ /** fetch detailed model data for all contents in this collection */
+ fetchAllDetails : function( options ){
+ options = options || {};
+ var detailsFlag = { details: 'all' };
+ options.data = _.extend( options.data || {}, detailsFlag );
+ return this.fetch( options );
+ },
+
+ // ............. quasi-batch ops
+ /** using a queue, perform ajaxFn on each of the models in this collection */
+ ajaxQueue : function( ajaxFn, options ){
+ var deferred = jQuery.Deferred(),
+ startingLength = this.length,
+ responses = [];
+
+ if( !startingLength ){
+ deferred.resolve([]);
+ return deferred;
+ }
+
+ // use reverse order (stylistic choice)
+ var ajaxFns = this.chain().reverse().map( function( content, i ){
+ return function(){
+ var xhr = ajaxFn.call( content, options );
+ // if successful, notify using the deferred to allow tracking progress
+ xhr.done( function( response ){
+ deferred.notify({ curr: i, total: startingLength, response: response, model: content });
+ });
+ // (regardless of previous error or success) if not last ajax call, shift and call the next
+ // if last fn, resolve deferred
+ xhr.always( function( response ){
+ responses.push( response );
+ if( ajaxFns.length ){
+ ajaxFns.shift()();
+ } else {
+ deferred.resolve( responses );
+ }
+ });
+ };
+ }).value();
+ // start the queue
+ ajaxFns.shift()();
+
+ return deferred;
+ },
+
+ /** */
+ progressivelyFetchDetails : function( options ){
+ // console.log( 'progressivelyFetchDetails:', options );
+ options = options || {};
+ var deferred = jQuery.Deferred();
+ var self = this;
+ var limit = options.limitPerCall || 50;
+ // TODO: only fetch tags and annotations if specifically requested
+ var searchAttributes = HDA_MODEL.HistoryDatasetAssociation.prototype.searchAttributes;
+ var detailKeys = searchAttributes.join( ',' );
+
+ // TODO: remove the need to maintain allFetched/lastFetched here
+ // by using fetchFirst/More here
+ function _notifyAndContinue( response, offset ){
+ // console.log( 'rcvd:', response.length );
+ deferred.notify( response, limit, offset );
+ if( !response.length || response.length < limit ){
+ self.allFetched = true;
+ deferred.resolve( response, limit, offset );
+ return;
+ }
+ _recursivelyFetch( offset + limit );
+ }
+
+ function _recursivelyFetch( offset ){
+ offset = offset || 0;
+ // console.log( '_recursivelyFetch:', offset );
+ var _options = _.extend( _.clone( options ), {
+ view : 'summary',
+ keys : detailKeys,
+ limit : limit,
+ offset : offset,
+ });
+
+ // console.log( 'fetchFn:', fetchFn + '' );
+ var fetchFn = offset === 0? self.fetchFirst : self.fetchMore;
+ // console.log( 'fetching:', _options.limit, _options.offset );
+ _.defer( function(){
+ fetchFn.call( self, _options )
+ .fail( deferred.reject )
+ .done( function( r ){ _notifyAndContinue( r, offset ); });
+ });
+ }
+ _recursivelyFetch();
+ return deferred;
+ },
+
+ /** */
+ isCopyable : function( contentsJSON ){
+ var copyableModelClasses = [
+ 'HistoryDatasetAssociation',
+ 'HistoryDatasetCollectionAssociation'
+ ];
+ return ( ( _.isObject( contentsJSON ) && contentsJSON.id )
+ && ( _.contains( copyableModelClasses, contentsJSON.model_class ) ) );
+ },
+
+ /** copy an existing, accessible hda into this collection */
+ copy : function( json ){
+ // TODO: somehow showhorn all this into 'save'
+ var id, type, contentType;
+ if( _.isString( json ) ){
+ id = json;
+ contentType = 'hda';
+ type = 'dataset';
+ } else {
+ id = json.id;
+ contentType = ({
+ 'HistoryDatasetAssociation' : 'hda',
+ 'LibraryDatasetDatasetAssociation' : 'ldda',
+ 'HistoryDatasetCollectionAssociation' : 'hdca'
+ })[ json.model_class ] || 'hda';
+ type = ( contentType === 'hdca'? 'dataset_collection' : 'dataset' );
+ }
+ var collection = this,
+ xhr = jQuery.post( this.url(), {
+ content : id,
+ source : contentType,
+ type : type,
+ view : 'detailed',
+ keys : 'create_time,update_time'
+ })
+ .done( function( response ){
+ collection.add([ response ], { parse: true });
+ })
+ .fail( function( error, status, message ){
+ collection.trigger( 'error', collection, xhr, {},
+ 'Error copying contents', { type: type, id: id, source: contentType });
+ });
+ return xhr;
+ },
+
+ /** */
+ createHDCA : function( elementIdentifiers, collectionType, name, options ){
+ //precondition: elementIdentifiers is an array of plain js objects
+ // in the proper form to create the collectionType
+ var contents = this,
+ typeToModel = {
+ list : HDCA_MODEL.HistoryListDatasetCollection,
+ paired : HDCA_MODEL.HistoryPairDatasetCollection
+ },
+ hdca = new (typeToModel[ collectionType ])({
+ history_id : this.historyId,
+ name : name,
+ // should probably be able to just send in a bunch of json here and restruct per class
+ element_identifiers : elementIdentifiers
+ });
+ // do I even need to use new above, can I just pass the attrs here
+ return hdca.save()
+ .done( function( response ){
+ contents.add( hdca );
+ })
+ .fail( function( xhr, status, message ){
+ contents.trigger( 'error', xhr, status, message );
+ });
+ },
+
+ // ........................................................................ searching
+ /** return true if all contents have the searchable attributes */
+ haveSearchDetails : function(){
+ return this.allFetched && this.all( function( content ){
+ // null (which is a valid returned annotation value)
+ // will return false when using content.has( 'annotation' )
+ //TODO: a bit hacky - formalize
+ return _.has( content.attributes, 'annotation' );
+ });
+ },
+
+ /** return a new collection of contents whose attributes contain the substring matchesWhat */
+ matches : function( matchesWhat ){
+ return this.filter( function( content ){
+ return content.matches( matchesWhat );
+ });
+ },
+
+ // ........................................................................ misc
+ /** In this override, copy the historyId to the clone */
+ clone : function(){
+ var clone = Backbone.Collection.prototype.clone.call( this );
+ clone.historyId = this.historyId;
+ return clone;
+ },
+
+ /** String representation. */
+ toString : function(){
+ return ([ 'HistoryContents(', [ this.historyId, this.length ].join(), ')' ].join( '' ));
+ }
+ });
+
+
+ //==============================================================================
+ return {
+ HistoryContents : HistoryContents
+ };
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! underscore */ 1), __webpack_require__(/*! jquery */ 3), __webpack_require__(/*! libs/backbone */ 2)))
+
+/***/ },
+/* 67 */
+/*!****************************************************************!*\
+ !*** ./galaxy/scripts/mvc/base/controlled-fetch-collection.js ***!
+ \****************************************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(jQuery) {!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
+ __webpack_require__(/*! libs/underscore */ 1),
+ __webpack_require__(/*! libs/backbone */ 2),
+ __webpack_require__(/*! mvc/base-mvc */ 5),
+ ], __WEBPACK_AMD_DEFINE_RESULT__ = function( _, Backbone, BASE_MVC ){
+ 'use strict';
+
+ //=============================================================================
+ /**
+ * A Collection that can be limited/offset/re-ordered/filtered.
+ * @type {Backbone.Collection}
+ */
+ var ControlledFetchCollection = Backbone.Collection.extend({
+
+ /** */
+ initialize : function( models, options ){
+ Backbone.Collection.prototype.initialize.call( this, models, options );
+ this.setOrder( options.order || this.order, { silent: true });
+ // this.on( 'all', function(){ console.log( this.toString(), arguments ); });
+ },
+
+ /** set up to track order changes and re-sort when changed */
+ _setUpListeners : function(){
+ return this.on({
+ 'changed-order' : this.sort
+ });
+ },
+
+ /** override to provide order and offsets based on instance vars, set limit if passed,
+ * and set allFetched/fire 'all-fetched' when xhr returns
+ */
+ fetch : function( options ){
+ options = this._buildFetchOptions( options );
+ return Backbone.Collection.prototype.fetch.call( this, options );
+ },
+
+ /** */
+ _buildFetchOptions : function( options ){
+ // note: we normally want options passed in to override the defaults built here
+ // so most of these fns will generate defaults
+ options = _.clone( options ) || {};
+ var self = this;
+
+ // options (options for backbone.fetch and jquery.ajax generally)
+ // backbone option; false here to make fetching an addititive process
+ options.remove = options.remove || false;
+ // jquery ajax option; allows multiple q/qv for filters (instead of 'q[]')
+ options.traditional = true;
+
+ // options.data
+ // we keep limit, offset, etc. in options *as well as move it into data* because:
+ // - it makes fetch calling convenient to add it to a single options map (instead of as mult. args)
+ // - it allows the std. event handlers (for fetch, etc.) to have access
+ // to the pagination options too
+ // (i.e. this.on( 'sync', function( options ){ if( options.limit ){ ... } }))
+ // however, when we send to xhr/jquery we copy them to data also so that they become API query params
+ options.data = options.data || self._buildFetchData( options );
+ // console.log( 'data:', options.data );
+
+ // options.data.filters --> options.data.q, options.data.qv
+ var filters = this._buildFetchFilters( options );
+ // console.log( 'filters:', filters );
+ if( !_.isEmpty( filters ) ){
+ _.extend( options.data, this._fetchFiltersToAjaxData( filters ) );
+ }
+ // console.log( 'data:', options.data );
+ return options;
+ },
+
+ /** Build the dictionary to send to fetch's XHR as data */
+ _buildFetchData : function( options ){
+ var defaults = {};
+ if( this.order ){ defaults.order = this.order; }
+ return _.defaults( _.pick( options, this._fetchParams ), defaults );
+ },
+
+ /** These attribute keys are valid params to fetch/API-index */
+ _fetchParams : [
+ /** model dependent string to control the order of models returned */
+ 'order',
+ /** limit the number of models returned from a fetch */
+ 'limit',
+ /** skip this number of models when fetching */
+ 'offset',
+ /** what series of attributes to return (model dependent) */
+ 'view',
+ /** individual keys to return for the models (see api/histories.index) */
+ 'keys'
+ ],
+
+ /** */
+ _buildFetchFilters : function( options ){
+ // override
+ return _.clone( options.filters || {} );
+ },
+
+ /** Convert dictionary filters to qqv style arrays */
+ _fetchFiltersToAjaxData : function( filters ){
+ // return as a map so ajax.data can extend from it
+ var filterMap = {
+ q : [],
+ qv : []
+ };
+ _.each( filters, function( v, k ){
+ // don't send if filter value is empty
+ if( v === undefined || v === '' ){ return; }
+ // json to python
+ if( v === true ){ v = 'True'; }
+ if( v === false ){ v = 'False'; }
+ if( v === null ){ v = 'None'; }
+ // map to k/v arrays (q/qv)
+ filterMap.q.push( k );
+ filterMap.qv.push( v );
+ });
+ return filterMap;
+ },
+
+ /** override to reset allFetched flag to false */
+ reset : function( models, options ){
+ this.allFetched = false;
+ return Backbone.Collection.prototype.reset.call( this, models, options );
+ },
+
+ // ........................................................................ order
+ order : null,
+
+ /** @type {Object} map of collection available sorting orders containing comparator fns */
+ comparators : {
+ 'update_time' : BASE_MVC.buildComparator( 'update_time', { ascending: false }),
+ 'update_time-asc' : BASE_MVC.buildComparator( 'update_time', { ascending: true }),
+ 'create_time' : BASE_MVC.buildComparator( 'create_time', { ascending: false }),
+ 'create_time-asc' : BASE_MVC.buildComparator( 'create_time', { ascending: true }),
+ },
+
+ /** set the order and comparator for this collection then sort with the new order
+ * @event 'changed-order' passed the new order and the collection
+ */
+ setOrder : function( order, options ){
+ options = options || {};
+ var collection = this;
+ var comparator = collection.comparators[ order ];
+ if( _.isUndefined( comparator ) ){ throw new Error( 'unknown order: ' + order ); }
+ // if( _.isUndefined( comparator ) ){ return; }
+ if( comparator === collection.comparator ){ return; }
+
+ var oldOrder = collection.order;
+ collection.order = order;
+ collection.comparator = comparator;
+
+ if( !options.silent ){
+ collection.trigger( 'changed-order', options );
+ }
+ return collection;
+ },
+ });
+
+
+ //=============================================================================
+ /**
+ * A Collection that is paginated when fetching.
+ */
+ var PaginatedCollection = ControlledFetchCollection.extend({
+
+ /** @type {Number} limit used for the first fetch (or a reset) */
+ limitOnFirstFetch : null,
+ /** @type {Number} limit used for each subsequent fetch */
+ limitPerFetch : 100,
+
+ /** */
+ initialize : function( models, options ){
+ ControlledFetchCollection.prototype.initialize.call( this, models, options );
+ this.limitOnFirstFetch = options.limitOnFirstFetch || this.limitOnFirstFetch;
+ this.limitPerFetch = options.limitPerFetch || this.limitPerFetch;
+ this.allFetched = false;
+ this.lastFetched = options.lastFetched || 0;
+ },
+
+ /** */
+ fetchFirst : function( options ){
+ // console.log( 'ControlledFetchCollection.fetchFirst:', options );
+ options = options? _.clone( options ) : {};
+ this.allFetched = false;
+ this.lastFetched = 0;
+ return this.fetchMore( _.extend( options, {
+ reset : true,
+ limit : this.limitOnFirstFetch,
+ }));
+ },
+
+ /** */
+ fetchMore : function( options ){
+ // console.log( 'ControlledFetchCollection.fetchMore:', options );
+ options = _.clone( options || {} );
+ var collection = this;
+
+ // console.log( 'fetchMore, options.reset:', options.reset );
+ if( ( !options.reset && collection.allFetched ) ){
+ console.warn( 'allFetched' ); return jQuery.when();
+ }
+
+ // TODO: this fails in the edge case where
+ // the first fetch offset === limit (limit 4, offset 4, collection.length 4)
+ options.offset = options.reset? 0 : collection.lastFetched;
+ var limit = options.limit = options.limit || collection.limitPerFetch || null;
+ // console.log( 'fetchMore, limit:', limit, 'offset:', options.offset );
+
+ collection.trigger( 'fetching-more' );
+ return collection.fetch( options )
+ .always( function(){
+ collection.trigger( 'fetching-more-done' );
+ })
+ // maintain allFetched flag and trigger if all were fetched this time
+ .done( function _postFetchMore( fetchedData ){
+ var numFetched = _.isArray( fetchedData )? fetchedData.length : 0;
+ collection.lastFetched += numFetched;
+ // console.log( 'fetchMore, lastFetched:', collection.lastFetched );
+ // anything less than a full page means we got all there is to get
+ if( !limit || numFetched < limit ){
+ collection.allFetched = true;
+ collection.trigger( 'all-fetched', this );
+ }
+ }
+ );
+ },
+
+ fetchAll : function( options ){
+ // whitelist options to prevent allowing limit/offset/filters
+ // (use vanilla fetch instead)
+ options = options || {};
+ var self = this;
+ options = _.pick( options, 'silent' );
+ //TODO: this doesn't work
+ options.filters = {};
+ return self.fetch( options ).done( function( fetchData ){
+ console.log( 'triggering:...' );
+ self.allFetched = true;
+ self.trigger( 'all-fetched', self );
+ });
+ },
+ });
+
+
+ //==============================================================================
+ return {
+ ControlledFetchCollection : ControlledFetchCollection,
+ PaginatedCollection : PaginatedCollection,
+ };
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! jquery */ 3)))
+
+/***/ },
+/* 68 */
+/*!*************************************************!*\
+ !*** ./galaxy/scripts/mvc/history/hda-model.js ***!
+ \*************************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(_) {!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
+ __webpack_require__(/*! mvc/dataset/dataset-model */ 69),
+ __webpack_require__(/*! mvc/history/history-content-model */ 71),
+ __webpack_require__(/*! mvc/base-mvc */ 5),
+ __webpack_require__(/*! utils/localization */ 7)
+ ], __WEBPACK_AMD_DEFINE_RESULT__ = function( DATASET, HISTORY_CONTENT, BASE_MVC, _l ){
+ 'use strict';
+
+ //==============================================================================
+ var _super = DATASET.DatasetAssociation,
+ hcontentMixin = HISTORY_CONTENT.HistoryContentMixin;
+ /** @class (HDA) model for a Galaxy dataset contained in and related to a history.
+ */
+ var HistoryDatasetAssociation = _super.extend( BASE_MVC.mixin( hcontentMixin,
+ /** @lends HistoryDatasetAssociation.prototype */{
+
+ /** default attributes for a model */
+ defaults : _.extend( {}, _super.prototype.defaults, hcontentMixin.defaults, {
+ model_class : 'HistoryDatasetAssociation'
+ }),
+ }));
+
+ //==============================================================================
+ return {
+ HistoryDatasetAssociation : HistoryDatasetAssociation
+ };
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! underscore */ 1)))
+
+/***/ },
+/* 69 */
+/*!*****************************************************!*\
+ !*** ./galaxy/scripts/mvc/dataset/dataset-model.js ***!
+ \*****************************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(Backbone, _, jQuery) {!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
+ __webpack_require__(/*! mvc/dataset/states */ 70),
+ __webpack_require__(/*! mvc/base-mvc */ 5),
+ __webpack_require__(/*! utils/localization */ 7)
+ ], __WEBPACK_AMD_DEFINE_RESULT__ = function( STATES, BASE_MVC, _l ){
+ 'use strict';
+
+ var logNamespace = 'dataset';
+ //==============================================================================
+ var searchableMixin = BASE_MVC.SearchableModelMixin;
+ /** @class base model for any DatasetAssociation (HDAs, LDDAs, DatasetCollectionDAs).
+ * No knowledge of what type (HDA/LDDA/DCDA) should be needed here.
+ * The DA's are made searchable (by attribute) by mixing in SearchableModelMixin.
+ */
+ var DatasetAssociation = Backbone.Model
+ .extend( BASE_MVC.LoggableMixin )
+ .extend( BASE_MVC.mixin( searchableMixin, /** @lends DatasetAssociation.prototype */{
+ _logNamespace : logNamespace,
+
+ /** default attributes for a model */
+ defaults : {
+ state : STATES.NEW,
+ deleted : false,
+ purged : false,
+
+ // unreliable attribute
+ name : '(unnamed dataset)',
+
+ //TODO: update to false when this is correctly passed from the API (when we have a security model for this)
+ accessible : true,
+
+ // sniffed datatype (sam, tabular, bed, etc.)
+ data_type : '',
+ file_ext : '',
+
+ // size in bytes
+ file_size : 0,
+
+ // array of associated file types (eg. [ 'bam_index', ... ])
+ meta_files : [],
+
+ misc_blurb : '',
+ misc_info : '',
+
+ tags : []
+ // do NOT default on annotation, as this default is valid and will be passed on 'save'
+ // which is incorrect behavior when the model is only partially fetched (annos are not passed in summary data)
+ //annotation : ''
+ },
+
+ /** instance vars and listeners */
+ initialize : function( attributes, options ){
+ this.debug( this + '(Dataset).initialize', attributes, options );
+
+ //!! this state is not in trans.app.model.Dataset.states - set it here -
+ if( !this.get( 'accessible' ) ){
+ this.set( 'state', STATES.NOT_VIEWABLE );
+ }
+
+ /** Datasets rely/use some web controllers - have the model generate those URLs on startup */
+ this.urls = this._generateUrls();
+
+ this._setUpListeners();
+ },
+
+ /** returns misc. web urls for rendering things like re-run, display, etc. */
+ _generateUrls : function(){
+ //TODO: would be nice if the API did this
+ var id = this.get( 'id' );
+ if( !id ){ return {}; }
+ var urls = {
+ 'purge' : 'datasets/' + id + '/purge_async',
+ 'display' : 'datasets/' + id + '/display/?preview=True',
+ 'edit' : 'datasets/' + id + '/edit',
+ 'download' : 'datasets/' + id + '/display?to_ext=' + this.get( 'file_ext' ),
+ 'report_error' : 'dataset/errors?id=' + id,
+ 'rerun' : 'tool_runner/rerun?id=' + id,
+ 'show_params' : 'datasets/' + id + '/show_params',
+ 'visualization' : 'visualization',
+ 'meta_download' : 'dataset/get_metadata_file?hda_id=' + id + '&metadata_name='
+ };
+ //TODO: global
+ _.each( urls, function( value, key ){
+ urls[ key ] = Galaxy.root + value;
+ });
+ this.urls = urls;
+ return urls;
+ },
+
+ /** set up any event listeners
+ * event: state:ready fired when this DA moves into/is already in a ready state
+ */
+ _setUpListeners : function(){
+ // if the state has changed and the new state is a ready state, fire an event
+ this.on( 'change:state', function( currModel, newState ){
+ this.log( this + ' has changed state:', currModel, newState );
+ if( this.inReadyState() ){
+ this.trigger( 'state:ready', currModel, newState, this.previous( 'state' ) );
+ }
+ });
+ // the download url (currently) relies on having a correct file extension
+ this.on( 'change:id change:file_ext', function( currModel ){
+ this._generateUrls();
+ });
+ },
+
+ // ........................................................................ common queries
+ /** override to add urls */
+ toJSON : function(){
+ var json = Backbone.Model.prototype.toJSON.call( this );
+ //console.warn( 'returning json?' );
+ //return json;
+ return _.extend( json, {
+ urls : this.urls
+ });
+ },
+
+ /** Is this dataset deleted or purged? */
+ isDeletedOrPurged : function(){
+ return ( this.get( 'deleted' ) || this.get( 'purged' ) );
+ },
+
+ /** Is this dataset in a 'ready' state; where 'Ready' states are states where no
+ * processing (for the ds) is left to do on the server.
+ */
+ inReadyState : function(){
+ var ready = _.contains( STATES.READY_STATES, this.get( 'state' ) );
+ return ( this.isDeletedOrPurged() || ready );
+ },
+
+ /** Does this model already contain detailed data (as opposed to just summary level data)? */
+ hasDetails : function(){
+ return this.has( 'annotation' );
+ },
+
+ /** Convenience function to match dataset.has_data. */
+ hasData : function(){
+ return ( this.get( 'file_size' ) > 0 );
+ },
+
+ // ........................................................................ ajax
+ fetch : function( options ){
+ var dataset = this;
+ return Backbone.Model.prototype.fetch.call( this, options )
+ .always( function(){
+ dataset._generateUrls();
+ });
+ },
+
+ /** override to use actual Dates objects for create/update times */
+ parse : function( response, options ){
+ var parsed = Backbone.Model.prototype.parse.call( this, response, options );
+ if( parsed.create_time ){
+ parsed.create_time = new Date( parsed.create_time );
+ }
+ if( parsed.update_time ){
+ parsed.update_time = new Date( parsed.update_time );
+ }
+ return parsed;
+ },
+
+ //NOTE: subclasses of DA's will need to implement url and urlRoot in order to have these work properly
+ /** save this dataset, _Mark_ing it as deleted (just a flag) */
+ 'delete' : function( options ){
+ if( this.get( 'deleted' ) ){ return jQuery.when(); }
+ return this.save( { deleted: true }, options );
+ },
+ /** save this dataset, _Mark_ing it as undeleted */
+ undelete : function( options ){
+ if( !this.get( 'deleted' ) || this.get( 'purged' ) ){ return jQuery.when(); }
+ return this.save( { deleted: false }, options );
+ },
+
+ /** remove the file behind this dataset from the filesystem (if permitted) */
+ purge : function _purge( options ){
+ //TODO: use, override model.destroy, HDA.delete({ purge: true })
+ if( this.get( 'purged' ) ){ return jQuery.when(); }
+ options = options || {};
+ //var hda = this,
+ // //xhr = jQuery.ajax( this.url() + '?' + jQuery.param({ purge: true }), _.extend({
+ // xhr = jQuery.ajax( this.url(), _.extend({
+ // type : 'DELETE',
+ // data : {
+ // purge : true
+ // }
+ // }, options ));
+ //
+ //xhr.done( function( response ){
+ // hda.debug( 'response', response );
+ // //hda.set({ deleted: true, purged: true });
+ // hda.set( response );
+ //});
+ //return xhr;
+
+ options.url = this.urls.purge;
+
+ //TODO: ideally this would be a DELETE call to the api
+ // using purge async for now
+ var hda = this,
+ xhr = jQuery.ajax( options );
+ xhr.done( function( message, status, responseObj ){
+ hda.set({ deleted: true, purged: true });
+ });
+ xhr.fail( function( xhr, status, message ){
+ // Exception messages are hidden within error page including: '...not allowed in this Galaxy instance.'
+ // unbury and re-add to xhr
+ var error = _l( "Unable to purge dataset" );
+ var messageBuriedInUnfortunatelyFormattedError = ( 'Removal of datasets by users '
+ + 'is not allowed in this Galaxy instance' );
+ if( xhr.responseJSON && xhr.responseJSON.error ){
+ error = xhr.responseJSON.error;
+ } else if( xhr.responseText.indexOf( messageBuriedInUnfortunatelyFormattedError ) !== -1 ){
+ error = messageBuriedInUnfortunatelyFormattedError;
+ }
+ xhr.responseText = error;
+ hda.trigger( 'error', hda, xhr, options, _l( error ), { error: error } );
+ });
+ return xhr;
+ },
+
+ // ........................................................................ searching
+ // see base-mvc, SearchableModelMixin
+
+ /** what attributes of an HDA will be used in a text search */
+ searchAttributes : [
+ 'name', 'file_ext', 'genome_build', 'misc_blurb', 'misc_info', 'annotation', 'tags'
+ ],
+
+ /** our attr keys don't often match the labels we display to the user - so, when using
+ * attribute specifiers ('name="bler"') in a term, allow passing in aliases for the
+ * following attr keys.
+ */
+ searchAliases : {
+ title : 'name',
+ format : 'file_ext',
+ database : 'genome_build',
+ blurb : 'misc_blurb',
+ description : 'misc_blurb',
+ info : 'misc_info',
+ tag : 'tags'
+ },
+
+ // ........................................................................ misc
+ /** String representation */
+ toString : function(){
+ var nameAndId = this.get( 'id' ) || '';
+ if( this.get( 'name' ) ){
+ nameAndId = '"' + this.get( 'name' ) + '",' + nameAndId;
+ }
+ return 'Dataset(' + nameAndId + ')';
+ }
+ }));
+
+
+ //==============================================================================
+ /** @class Backbone collection for dataset associations.
+ */
+ var DatasetAssociationCollection = Backbone.Collection.extend( BASE_MVC.LoggableMixin ).extend(
+ /** @lends HistoryContents.prototype */{
+ _logNamespace : logNamespace,
+
+ model : DatasetAssociation,
+
+ /** root api url */
+ urlRoot : Galaxy.root + 'api/datasets',
+
+ /** url fn */
+ url : function(){
+ return this.urlRoot;
+ },
+
+ // ........................................................................ common queries
+ /** Get the ids of every item in this collection
+ * @returns array of encoded ids
+ */
+ ids : function(){
+ return this.map( function( item ){ return item.get('id'); });
+ },
+
+ /** Get contents that are not ready
+ * @returns array of content models
+ */
+ notReady : function(){
+ return this.filter( function( content ){
+ return !content.inReadyState();
+ });
+ },
+
+ /** return true if any datasets don't have details */
+ haveDetails : function(){
+ return this.all( function( dataset ){ return dataset.hasDetails(); });
+ },
+
+ // ........................................................................ ajax
+ /** using a queue, perform ajaxFn on each of the models in this collection */
+ ajaxQueue : function( ajaxFn, options ){
+ var deferred = jQuery.Deferred(),
+ startingLength = this.length,
+ responses = [];
+
+ if( !startingLength ){
+ deferred.resolve([]);
+ return deferred;
+ }
+
+ // use reverse order (stylistic choice)
+ var ajaxFns = this.chain().reverse().map( function( dataset, i ){
+ return function(){
+ var xhr = ajaxFn.call( dataset, options );
+ // if successful, notify using the deferred to allow tracking progress
+ xhr.done( function( response ){
+ deferred.notify({ curr: i, total: startingLength, response: response, model: dataset });
+ });
+ // (regardless of previous error or success) if not last ajax call, shift and call the next
+ // if last fn, resolve deferred
+ xhr.always( function( response ){
+ responses.push( response );
+ if( ajaxFns.length ){
+ ajaxFns.shift()();
+ } else {
+ deferred.resolve( responses );
+ }
+ });
+ };
+ }).value();
+ // start the queue
+ ajaxFns.shift()();
+
+ return deferred;
+ },
+
+ // ........................................................................ sorting/filtering
+ /** return a new collection of datasets whose attributes contain the substring matchesWhat */
+ matches : function( matchesWhat ){
+ return this.filter( function( dataset ){
+ return dataset.matches( matchesWhat );
+ });
+ },
+
+ // ........................................................................ misc
+ ///** Convert this ad-hoc collection of hdas to a formal collection tracked
+ // by the server.
+ //**/
+ //promoteToHistoryDatasetCollection : function _promote( history, collection_type, options ){
+ //},
+
+ /** String representation. */
+ toString : function(){
+ return ([ 'DatasetAssociationCollection(', this.length, ')' ].join( '' ));
+ }
+ });
+
+
+ //==============================================================================
+ return {
+ DatasetAssociation : DatasetAssociation,
+ DatasetAssociationCollection : DatasetAssociationCollection
+ };
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! libs/backbone */ 2), __webpack_require__(/*! underscore */ 1), __webpack_require__(/*! jquery */ 3)))
+
+/***/ },
+/* 70 */
+/*!**********************************************!*\
+ !*** ./galaxy/scripts/mvc/dataset/states.js ***!
+ \**********************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
+ ], __WEBPACK_AMD_DEFINE_RESULT__ = function(){
+
+ 'use strict';
+ //==============================================================================
+ /** Map of possible HDA/collection/job states to their string equivalents.
+ * A port of galaxy.model.Dataset.states.
+ */
+ var STATES = {
+ // NOT ready states
+ /** is uploading and not ready */
+ UPLOAD : 'upload',
+ /** the job that will produce the dataset queued in the runner */
+ QUEUED : 'queued',
+ /** the job that will produce the dataset is running */
+ RUNNING : 'running',
+ /** metadata for the dataset is being discovered/set */
+ SETTING_METADATA : 'setting_metadata',
+
+ // ready states
+ /** was created without a tool */
+ NEW : 'new',
+ /** has no data */
+ EMPTY : 'empty',
+ /** has successfully completed running */
+ OK : 'ok',
+
+ /** the job that will produce the dataset paused */
+ PAUSED : 'paused',
+ /** metadata discovery/setting failed or errored (but otherwise ok) */
+ FAILED_METADATA : 'failed_metadata',
+ //TODO: not in trans.app.model.Dataset.states - is in database
+ /** not accessible to the current user (i.e. due to permissions) */
+ NOT_VIEWABLE : 'noPermission',
+ /** deleted while uploading */
+ DISCARDED : 'discarded',
+ /** the tool producing this dataset failed */
+ ERROR : 'error'
+ };
+
+ STATES.READY_STATES = [
+ STATES.OK,
+ STATES.EMPTY,
+ STATES.PAUSED,
+ STATES.FAILED_METADATA,
+ STATES.NOT_VIEWABLE,
+ STATES.DISCARDED,
+ STATES.ERROR
+ ];
+
+ STATES.NOT_READY_STATES = [
+ STATES.UPLOAD,
+ STATES.QUEUED,
+ STATES.RUNNING,
+ STATES.SETTING_METADATA,
+ STATES.NEW
+ ];
+
+
+ //==============================================================================
+ return STATES;
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+
+/***/ },
+/* 71 */
+/*!*************************************************************!*\
+ !*** ./galaxy/scripts/mvc/history/history-content-model.js ***!
+ \*************************************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(jQuery) {!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
+ __webpack_require__(/*! mvc/dataset/states */ 70),
+ __webpack_require__(/*! mvc/base-mvc */ 5),
+ __webpack_require__(/*! utils/localization */ 7)
+ ], __WEBPACK_AMD_DEFINE_RESULT__ = function( STATES, BASE_MVC, _l ){
+ 'use strict';
+
+ //==============================================================================
+ /** @class Mixin for HistoryContents content (HDAs, HDCAs).
+ */
+ var HistoryContentMixin = {
+
+ /** default attributes for a model */
+ defaults : {
+ /** parent (containing) history */
+ history_id : null,
+ /** some content_type (HistoryContents can contain mixed model classes) */
+ history_content_type: null,
+ /** indicating when/what order the content was generated in the context of the history */
+ hid : null,
+ /** whether the user wants the content shown (visible) */
+ visible : true
+ },
+
+ // ........................................................................ mixed content element
+ // In order to be part of a MIXED bbone collection, we can't rely on the id
+ // (which may collide btwn models of different classes)
+ // Instead, use type_id which prefixes the history_content_type so the bbone collection can differentiate
+ idAttribute : 'type_id',
+
+ /** override because backbone tests boolean( idAttribute ), but it's not an empty string even for new models
+ * due to our use of type_id.
+ */
+ // isNew : function(){
+ // return !this.get( 'id' );
+ // },
+
+ // ........................................................................ common queries
+ /** the more common alias of visible */
+ hidden : function(){
+ return !this.get( 'visible' );
+ },
+
+ /** based on show_deleted, show_hidden (gen. from the container control),
+ * would this ds show in the list of ds's?
+ * @param {Boolean} show_deleted are we showing deleted hdas?
+ * @param {Boolean} show_hidden are we showing hidden hdas?
+ */
+ isVisible : function( show_deleted, show_hidden ){
+ var isVisible = true;
+ if( ( !show_deleted )
+ && ( this.get( 'deleted' ) || this.get( 'purged' ) ) ){
+ isVisible = false;
+ }
+ if( ( !show_hidden )
+ && ( !this.get( 'visible' ) ) ){
+ isVisible = false;
+ }
+ return isVisible;
+ },
+
+ // ........................................................................ ajax
+ //TODO?: these are probably better done on the leaf classes
+ /** history content goes through the 'api/histories' API */
+ urlRoot: Galaxy.root + 'api/histories/',
+
+ /** full url spec. for this content */
+ url : function(){
+ var url = this.urlRoot + this.get( 'history_id' ) + '/contents/'
+ + this.get('history_content_type') + 's/' + this.get( 'id' );
+ return url;
+ },
+
+ /** save this content as not visible */
+ hide : function( options ){
+ if( !this.get( 'visible' ) ){ return jQuery.when(); }
+ return this.save( { visible: false }, options );
+ },
+ /** save this content as visible */
+ unhide : function( options ){
+ if( this.get( 'visible' ) ){ return jQuery.when(); }
+ return this.save( { visible: true }, options );
+ },
+
+ // ........................................................................ misc
+ toString : function(){
+ return ([ this.get( 'type_id' ), this.get( 'hid' ), this.get( 'name' ) ].join(':'));
+ }
+ };
+
+
+ //==============================================================================
+ return {
+ HistoryContentMixin : HistoryContentMixin
+ };
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! jquery */ 3)))
+
+/***/ },
+/* 72 */
+/*!**************************************************!*\
+ !*** ./galaxy/scripts/mvc/history/hdca-model.js ***!
+ \**************************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(_) {!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
+ __webpack_require__(/*! mvc/collection/collection-model */ 73),
+ __webpack_require__(/*! mvc/history/history-content-model */ 71),
+ __webpack_require__(/*! utils/localization */ 7)
+ ], __WEBPACK_AMD_DEFINE_RESULT__ = function( DC_MODEL, HISTORY_CONTENT, _l ){
+
+ 'use strict';
+
+ /*==============================================================================
+
+ Models for DatasetCollections contained within a history.
+
+ TODO:
+ these might be compactable to one class if some duplication with
+ collection-model is used.
+
+ ==============================================================================*/
+ var hcontentMixin = HISTORY_CONTENT.HistoryContentMixin,
+ ListDC = DC_MODEL.ListDatasetCollection,
+ PairDC = DC_MODEL.PairDatasetCollection,
+ ListPairedDC = DC_MODEL.ListPairedDatasetCollection;
+
+ //==============================================================================
+ /** Override to post to contents route w/o id. */
+ function buildHDCASave( _super ){
+ return function _save( attributes, options ){
+ if( this.isNew() ){
+ options = options || {};
+ options.url = this.urlRoot + this.get( 'history_id' ) + '/contents';
+ attributes = attributes || {};
+ attributes.type = 'dataset_collection';
+ }
+ return _super.call( this, attributes, options );
+ };
+ }
+
+
+ //==============================================================================
+ /** @class Backbone model for List Dataset Collection within a History.
+ */
+ var HistoryListDatasetCollection = ListDC.extend( hcontentMixin ).extend(
+ /** @lends HistoryListDatasetCollection.prototype */{
+
+ defaults : _.extend( _.clone( ListDC.prototype.defaults ), {
+ history_content_type: 'dataset_collection',
+ collection_type : 'list',
+ model_class : 'HistoryDatasetCollectionAssociation'
+ }),
+
+ /** Override to post to contents route w/o id. */
+ save : buildHDCASave( ListDC.prototype.save ),
+
+ /** String representation. */
+ toString : function(){
+ return 'History' + ListDC.prototype.toString.call( this );
+ }
+ });
+
+
+ //==============================================================================
+ /** @class Backbone model for Pair Dataset Collection within a History.
+ * @constructs
+ */
+ var HistoryPairDatasetCollection = PairDC.extend( hcontentMixin ).extend(
+ /** @lends HistoryPairDatasetCollection.prototype */{
+
+ defaults : _.extend( _.clone( PairDC.prototype.defaults ), {
+ history_content_type: 'dataset_collection',
+ collection_type : 'paired',
+ model_class : 'HistoryDatasetCollectionAssociation'
+ }),
+
+ /** Override to post to contents route w/o id. */
+ save : buildHDCASave( PairDC.prototype.save ),
+
+ /** String representation. */
+ toString : function(){
+ return 'History' + PairDC.prototype.toString.call( this );
+ }
+ });
+
+
+ //==============================================================================
+ /** @class Backbone model for List of Pairs Dataset Collection within a History.
+ * @constructs
+ */
+ var HistoryListPairedDatasetCollection = ListPairedDC.extend( hcontentMixin ).extend(
+ /** @lends HistoryListPairedDatasetCollection.prototype */{
+
+ defaults : _.extend( _.clone( ListPairedDC.prototype.defaults ), {
+ history_content_type: 'dataset_collection',
+ collection_type : 'list:paired',
+ model_class : 'HistoryDatasetCollectionAssociation'
+ }),
+
+ /** Override to post to contents route w/o id. */
+ save : buildHDCASave( ListPairedDC.prototype.save ),
+
+ /** String representation. */
+ toString : function(){
+ return 'History' + ListPairedDC.prototype.toString.call( this );
+ }
+ });
+
+
+ //==============================================================================
+ return {
+ HistoryListDatasetCollection : HistoryListDatasetCollection,
+ HistoryPairDatasetCollection : HistoryPairDatasetCollection,
+ HistoryListPairedDatasetCollection : HistoryListPairedDatasetCollection
+ };
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! underscore */ 1)))
+
+/***/ },
+/* 73 */
+/*!***********************************************************!*\
+ !*** ./galaxy/scripts/mvc/collection/collection-model.js ***!
+ \***********************************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(_, Backbone, jQuery) {!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
+ __webpack_require__(/*! mvc/dataset/dataset-model */ 69),
+ __webpack_require__(/*! mvc/base-mvc */ 5),
+ __webpack_require__(/*! utils/localization */ 7)
+ ], __WEBPACK_AMD_DEFINE_RESULT__ = function( DATASET_MODEL, BASE_MVC, _l ){
+ 'use strict';
+
+ //==============================================================================
+ /*
+ Notes:
+
+ Terminology:
+ DatasetCollection/DC : a container of datasets or nested DatasetCollections
+ Element/DatasetCollectionElement/DCE : an item contained in a DatasetCollection
+ HistoryDatasetCollectionAssociation/HDCA: a DatasetCollection contained in a history
+
+
+ This all seems too complex unfortunately:
+
+ - Terminology collision between DatasetCollections (DCs) and Backbone Collections.
+ - In the DatasetCollections API JSON, DC Elements use a 'Has A' stucture to *contain*
+ either a dataset or a nested DC. This would make the hierarchy much taller. I've
+ decided to merge the contained JSON with the DC element json - making the 'has a'
+ relation into an 'is a' relation. This seems simpler to me and allowed a lot of
+ DRY in both models and views, but may make tracking or tracing within these models
+ more difficult (since DatasetCollectionElements are now *also* DatasetAssociations
+ or DatasetCollections (nested)). This also violates the rule of thumb about
+ favoring aggregation over inheritance.
+ - Currently, there are three DatasetCollection subclasses: List, Pair, and ListPaired.
+ These each should a) be usable on their own, b) be usable in the context of
+ nesting within a collection model (at least in the case of ListPaired), and
+ c) be usable within the context of other container models (like History or
+ LibraryFolder, etc.). I've tried to separate/extract classes in order to
+ handle those three situations, but it's proven difficult to do in a simple,
+ readable manner.
+ - Ideally, histories and libraries would inherit from the same server models as
+ dataset collections do since they are (in essence) dataset collections themselves -
+ making the whole nested structure simpler. This would be a large, error-prone
+ refactoring and migration.
+
+ Many of the classes and heirarchy are meant as extension points so, while the
+ relations and flow may be difficult to understand initially, they'll allow us to
+ handle the growth or flux dataset collection in the future (w/o actually implementing
+ any YAGNI).
+
+ */
+ //_________________________________________________________________________________________________ ELEMENTS
+ /** @class mixin for Dataset collection elements.
+ * When collection elements are passed from the API, the underlying element is
+ * in a sub-object 'object' (IOW, a DCE representing an HDA will have HDA json in element.object).
+ * This mixin uses the constructor and parse methods to merge that JSON with the DCE attribtues
+ * effectively changing a DCE from a container to a subclass (has a --> is a).
+ */
+ var DatasetCollectionElementMixin = {
+
+ /** default attributes used by elements in a dataset collection */
+ defaults : {
+ model_class : 'DatasetCollectionElement',
+ element_identifier : null,
+ element_index : null,
+ element_type : null
+ },
+
+ /** merge the attributes of the sub-object 'object' into this model */
+ _mergeObject : function( attributes ){
+ // if we don't preserve and correct ids here, the element id becomes the object id
+ // and collision in backbone's _byId will occur and only
+ _.extend( attributes, attributes.object, { element_id: attributes.id });
+ delete attributes.object;
+ return attributes;
+ },
+
+ /** override to merge this.object into this */
+ constructor : function( attributes, options ){
+ // console.debug( '\t DatasetCollectionElement.constructor:', attributes, options );
+ attributes = this._mergeObject( attributes );
+ this.idAttribute = 'element_id';
+ Backbone.Model.apply( this, arguments );
+ },
+
+ /** when the model is fetched, merge this.object into this */
+ parse : function( response, options ){
+ var attributes = response;
+ attributes = this._mergeObject( attributes );
+ return attributes;
+ }
+ };
+
+ /** @class Concrete class of Generic DatasetCollectionElement */
+ var DatasetCollectionElement = Backbone.Model
+ .extend( BASE_MVC.LoggableMixin )
+ .extend( DatasetCollectionElementMixin )
+ .extend({ _logNamespace : 'collections' });
+
+
+ //==============================================================================
+ /** @class Base/Abstract Backbone collection for Generic DCEs. */
+ var DCECollection = Backbone.Collection.extend( BASE_MVC.LoggableMixin ).extend(
+ /** @lends DCECollection.prototype */{
+ _logNamespace : 'collections',
+
+ model: DatasetCollectionElement,
+
+ /** String representation. */
+ toString : function(){
+ return ([ 'DatasetCollectionElementCollection(', this.length, ')' ].join( '' ));
+ }
+ });
+
+
+ //==============================================================================
+ /** @class Backbone model for a dataset collection element that is a dataset (HDA).
+ */
+ var DatasetDCE = DATASET_MODEL.DatasetAssociation.extend( BASE_MVC.mixin( DatasetCollectionElementMixin,
+ /** @lends DatasetDCE.prototype */{
+
+ /** url fn */
+ url : function(){
+ // won't always be an hda
+ if( !this.has( 'history_id' ) ){
+ console.warn( 'no endpoint for non-hdas within a collection yet' );
+ // (a little silly since this api endpoint *also* points at hdas)
+ return Galaxy.root + 'api/datasets';
+ }
+ return Galaxy.root + 'api/histories/' + this.get( 'history_id' ) + '/contents/' + this.get( 'id' );
+ },
+
+ defaults : _.extend( {},
+ DATASET_MODEL.DatasetAssociation.prototype.defaults,
+ DatasetCollectionElementMixin.defaults
+ ),
+
+ // because all objects have constructors (as this hashmap would even if this next line wasn't present)
+ // the constructor in hcontentMixin won't be attached by BASE_MVC.mixin to this model
+ // - re-apply manually for now
+ /** call the mixin constructor */
+ constructor : function( attributes, options ){
+ this.debug( '\t DatasetDCE.constructor:', attributes, options );
+ //DATASET_MODEL.DatasetAssociation.prototype.constructor.call( this, attributes, options );
+ DatasetCollectionElementMixin.constructor.call( this, attributes, options );
+ },
+
+ /** Does this model already contain detailed data (as opposed to just summary level data)? */
+ hasDetails : function(){
+ return this.elements && this.elements.length;
+ },
+
+ /** String representation. */
+ toString : function(){
+ var objStr = this.get( 'element_identifier' );
+ return ([ 'DatasetDCE(', objStr, ')' ].join( '' ));
+ }
+ }));
+
+
+ //==============================================================================
+ /** @class DCECollection of DatasetDCE's (a list of datasets, a pair of datasets).
+ */
+ var DatasetDCECollection = DCECollection.extend(
+ /** @lends DatasetDCECollection.prototype */{
+ model: DatasetDCE,
+
+ /** String representation. */
+ toString : function(){
+ return ([ 'DatasetDCECollection(', this.length, ')' ].join( '' ));
+ }
+ });
+
+
+ //_________________________________________________________________________________________________ COLLECTIONS
+ /** @class Backbone model for Dataset Collections.
+ * The DC API returns an array of JSON objects under the attribute elements.
+ * This model:
+ * - removes that array/attribute ('elements') from the model,
+ * - creates a bbone collection (of the class defined in the 'collectionClass' attribute),
+ * - passes that json onto the bbone collection
+ * - caches the bbone collection in this.elements
+ */
+ var DatasetCollection = Backbone.Model
+ .extend( BASE_MVC.LoggableMixin )
+ .extend( BASE_MVC.SearchableModelMixin )
+ .extend(/** @lends DatasetCollection.prototype */{
+ _logNamespace : 'collections',
+
+ /** logger used to record this.log messages, commonly set to console */
+ //logger : console,
+
+ /** default attributes for a model */
+ defaults : {
+ /* 'list', 'paired', or 'list:paired' */
+ collection_type : null,
+ //??
+ deleted : false
+ },
+
+ /** Which class to use for elements */
+ collectionClass : DCECollection,
+
+ /** set up: create elements instance var and (on changes to elements) update them */
+ initialize : function( model, options ){
+ this.debug( this + '(DatasetCollection).initialize:', model, options, this );
+ this.elements = this._createElementsModel();
+ this.on( 'change:elements', function(){
+ this.log( 'change:elements' );
+ //TODO: prob. better to update the collection instead of re-creating it
+ this.elements = this._createElementsModel();
+ });
+ },
+
+ /** move elements model attribute to full collection */
+ _createElementsModel : function(){
+ this.debug( this + '._createElementsModel', this.collectionClass, this.get( 'elements' ), this.elements );
+ //TODO: same patterns as DatasetCollectionElement _createObjectModel - refactor to BASE_MVC.hasSubModel?
+ var elements = this.get( 'elements' ) || [];
+ this.unset( 'elements', { silent: true });
+ this.elements = new this.collectionClass( elements );
+ //this.debug( 'collectionClass:', this.collectionClass + '', this.elements );
+ return this.elements;
+ },
+
+ // ........................................................................ common queries
+ /** pass the elements back within the model json when this is serialized */
+ toJSON : function(){
+ var json = Backbone.Model.prototype.toJSON.call( this );
+ if( this.elements ){
+ json.elements = this.elements.toJSON();
+ }
+ return json;
+ },
+
+ /** Is this collection in a 'ready' state no processing (for the collection) is left
+ * to do on the server.
+ */
+ inReadyState : function(){
+ var populated = this.get( 'populated' );
+ return ( this.isDeletedOrPurged() || populated );
+ },
+
+ //TODO:?? the following are the same interface as DatasetAssociation - can we combine?
+ /** Does the DC contain any elements yet? Is a fetch() required? */
+ hasDetails : function(){
+ return this.has( 'annotation' );
+ },
+
+ /** Given the filters, what models in this.elements would be returned? */
+ getVisibleContents : function( filters ){
+ // filters unused for now
+ return this.elements;
+ },
+
+ // ........................................................................ ajax
+ /** override to use actual Dates objects for create/update times */
+ parse : function( response, options ){
+ var parsed = Backbone.Model.prototype.parse.call( this, response, options );
+ if( parsed.create_time ){
+ parsed.create_time = new Date( parsed.create_time );
+ }
+ if( parsed.update_time ){
+ parsed.update_time = new Date( parsed.update_time );
+ }
+ return parsed;
+ },
+
+ /** save this dataset, _Mark_ing it as deleted (just a flag) */
+ 'delete' : function( options ){
+ if( this.get( 'deleted' ) ){ return jQuery.when(); }
+ return this.save( { deleted: true }, options );
+ },
+ /** save this dataset, _Mark_ing it as undeleted */
+ undelete : function( options ){
+ if( !this.get( 'deleted' ) || this.get( 'purged' ) ){ return jQuery.when(); }
+ return this.save( { deleted: false }, options );
+ },
+
+ /** Is this collection deleted or purged? */
+ isDeletedOrPurged : function(){
+ return ( this.get( 'deleted' ) || this.get( 'purged' ) );
+ },
+
+ // ........................................................................ searchable
+ /** searchable attributes for collections */
+ searchAttributes : [
+ 'name'
+ ],
+
+ // ........................................................................ misc
+ /** String representation */
+ toString : function(){
+ var idAndName = [ this.get( 'id' ), this.get( 'name' ) || this.get( 'element_identifier' ) ];
+ return 'DatasetCollection(' + ( idAndName.join(',') ) + ')';
+ }
+ });
+
+
+ //==============================================================================
+ /** Model for a DatasetCollection containing datasets (non-nested).
+ */
+ var ListDatasetCollection = DatasetCollection.extend(
+ /** @lends ListDatasetCollection.prototype */{
+
+ /** override since we know the collection will only contain datasets */
+ collectionClass : DatasetDCECollection,
+
+ /** String representation. */
+ toString : function(){ return 'List' + DatasetCollection.prototype.toString.call( this ); }
+ });
+
+
+ //==============================================================================
+ /** Model for a DatasetCollection containing fwd/rev datasets (a list of 2).
+ */
+ var PairDatasetCollection = ListDatasetCollection.extend(
+ /** @lends PairDatasetCollection.prototype */{
+
+ /** String representation. */
+ toString : function(){ return 'Pair' + DatasetCollection.prototype.toString.call( this ); }
+ });
+
+
+ //_________________________________________________________________________________________________ NESTED COLLECTIONS
+ // this is where things get weird, man. Weird.
+ //TODO: it might be possible to compact all the following...I think.
+ //==============================================================================
+ /** @class Backbone model for a Generic DatasetCollectionElement that is also a DatasetCollection
+ * (a nested collection). Currently only list:paired.
+ */
+ var NestedDCDCE = DatasetCollection.extend( BASE_MVC.mixin( DatasetCollectionElementMixin,
+ /** @lends NestedDCDCE.prototype */{
+
+ // because all objects have constructors (as this hashmap would even if this next line wasn't present)
+ // the constructor in hcontentMixin won't be attached by BASE_MVC.mixin to this model
+ // - re-apply manually it now
+ /** call the mixin constructor */
+ constructor : function( attributes, options ){
+ this.debug( '\t NestedDCDCE.constructor:', attributes, options );
+ DatasetCollectionElementMixin.constructor.call( this, attributes, options );
+ },
+
+ /** String representation. */
+ toString : function(){
+ var objStr = ( this.object )?( '' + this.object ):( this.get( 'element_identifier' ) );
+ return ([ 'NestedDCDCE(', objStr, ')' ].join( '' ));
+ }
+ }));
+
+
+ //==============================================================================
+ /** @class Backbone collection containing Generic NestedDCDCE's (nested dataset collections).
+ */
+ var NestedDCDCECollection = DCECollection.extend(
+ /** @lends NestedDCDCECollection.prototype */{
+
+ /** This is a collection of nested collections */
+ model: NestedDCDCE,
+
+ /** String representation. */
+ toString : function(){
+ return ([ 'NestedDCDCECollection(', this.length, ')' ].join( '' ));
+ }
+ });
+
+
+ //==============================================================================
+ /** @class Backbone model for a paired dataset collection within a list:paired dataset collection.
+ */
+ var NestedPairDCDCE = PairDatasetCollection.extend( BASE_MVC.mixin( DatasetCollectionElementMixin,
+ /** @lends NestedPairDCDCE.prototype */{
+ //TODO:?? possibly rename to NestedDatasetCollection?
+
+ // because all objects have constructors (as this hashmap would even if this next line wasn't present)
+ // the constructor in hcontentMixin won't be attached by BASE_MVC.mixin to this model
+ // - re-apply manually it now
+ /** This is both a collection and a collection element - call the constructor */
+ constructor : function( attributes, options ){
+ this.debug( '\t NestedPairDCDCE.constructor:', attributes, options );
+ //DatasetCollection.constructor.call( this, attributes, options );
+ DatasetCollectionElementMixin.constructor.call( this, attributes, options );
+ },
+
+ /** String representation. */
+ toString : function(){
+ var objStr = ( this.object )?( '' + this.object ):( this.get( 'element_identifier' ) );
+ return ([ 'NestedPairDCDCE(', objStr, ')' ].join( '' ));
+ }
+ }));
+
+
+ //==============================================================================
+ /** @class Backbone collection for a backbone collection containing paired dataset collections.
+ */
+ var NestedPairDCDCECollection = NestedDCDCECollection.extend(
+ /** @lends PairDCDCECollection.prototype */{
+
+ /** We know this collection is composed of only nested pair collections */
+ model: NestedPairDCDCE,
+
+ /** String representation. */
+ toString : function(){
+ return ([ 'NestedPairDCDCECollection(', this.length, ')' ].join( '' ));
+ }
+ });
+
+
+ //==============================================================================
+ /** @class Backbone Model for a DatasetCollection (list) that contains DatasetCollections (pairs).
+ */
+ var ListPairedDatasetCollection = DatasetCollection.extend(
+ /** @lends ListPairedDatasetCollection.prototype */{
+
+ /** list:paired is the only collection that itself contains collections */
+ collectionClass : NestedPairDCDCECollection,
+
+ /** String representation. */
+ toString : function(){
+ return ([ 'ListPairedDatasetCollection(', this.get( 'name' ), ')' ].join( '' ));
+ }
+ });
+
+
+ //==============================================================================
+ return {
+ ListDatasetCollection : ListDatasetCollection,
+ PairDatasetCollection : PairDatasetCollection,
+ ListPairedDatasetCollection : ListPairedDatasetCollection
+ };
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! underscore */ 1), __webpack_require__(/*! libs/backbone */ 2), __webpack_require__(/*! jquery */ 3)))
+
+/***/ },
+/* 74 */
+/*!***********************************************************!*\
+ !*** ./galaxy/scripts/mvc/history/history-preferences.js ***!
+ \***********************************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(_) {!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
+ __webpack_require__(/*! mvc/base-mvc */ 5)
+ ], __WEBPACK_AMD_DEFINE_RESULT__ = function( BASE_MVC ){
+
+ 'use strict';
+
+ var logNamespace = 'history';
+
+ // ============================================================================
+ /** session storage for individual history preferences */
+ var HistoryPrefs = BASE_MVC.SessionStorageModel.extend(
+ /** @lends HistoryPrefs.prototype */{
+ //TODO:?? possibly mark as current T/F - have History.currId() (a class method) return that value
+ defaults : {
+ //TODO:?? expandedIds to array?
+ expandedIds : {},
+ //TODO:?? move to user?
+ show_deleted : false,
+ show_hidden : false
+ //TODO: add scroll position?
+ },
+
+ /** add an hda id to the hash of expanded hdas */
+ addExpanded : function( model ){
+ var key = 'expandedIds';
+ //TODO:?? is this right anymore?
+ this.save( key, _.extend( this.get( key ), _.object([ model.id ], [ model.get( 'id' ) ]) ) );
+ },
+
+ /** remove an hda id from the hash of expanded hdas */
+ removeExpanded : function( model ){
+ var key = 'expandedIds';
+ this.save( key, _.omit( this.get( key ), model.id ) );
+ },
+
+ toString : function(){
+ return 'HistoryPrefs(' + this.id + ')';
+ }
+
+ }, {
+ // ........................................................................ class vars
+ // class lvl for access w/o instantiation
+ storageKeyPrefix : 'history:',
+
+ /** key string to store each histories settings under */
+ historyStorageKey : function historyStorageKey( historyId ){
+ if( !historyId ){
+ throw new Error( 'HistoryPrefs.historyStorageKey needs valid id: ' + historyId );
+ }
+ // single point of change
+ return ( HistoryPrefs.storageKeyPrefix + historyId );
+ },
+
+ /** return the existing storage for the history with the given id (or create one if it doesn't exist) */
+ get : function get( historyId ){
+ return new HistoryPrefs({ id: HistoryPrefs.historyStorageKey( historyId ) });
+ },
+
+ /** clear all history related items in sessionStorage */
+ clearAll : function clearAll( historyId ){
+ for( var key in sessionStorage ){
+ if( key.indexOf( HistoryPrefs.storageKeyPrefix ) === 0 ){
+ sessionStorage.removeItem( key );
+ }
+ }
+ }
+ });
+
+ //==============================================================================
+ return {
+ HistoryPrefs: HistoryPrefs
+ };
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! underscore */ 1)))
+
+/***/ },
+/* 75 */
+/*!*********************************************************!*\
+ !*** ./galaxy/scripts/mvc/history/history-view-edit.js ***!
+ \*********************************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(jQuery, _, $) {!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
+ __webpack_require__(/*! mvc/history/history-view */ 79),
+ __webpack_require__(/*! mvc/history/history-contents */ 66),
+ __webpack_require__(/*! mvc/dataset/states */ 70),
+ __webpack_require__(/*! mvc/history/hda-model */ 68),
+ __webpack_require__(/*! mvc/history/hda-li-edit */ 90),
+ __webpack_require__(/*! mvc/history/hdca-li-edit */ 95),
+ __webpack_require__(/*! mvc/tag */ 92),
+ __webpack_require__(/*! mvc/annotation */ 93),
+ __webpack_require__(/*! mvc/collection/list-collection-creator */ 76),
+ __webpack_require__(/*! mvc/collection/pair-collection-creator */ 98),
+ __webpack_require__(/*! mvc/collection/list-of-pairs-collection-creator */ 99),
+ __webpack_require__(/*! ui/fa-icon-button */ 86),
+ __webpack_require__(/*! mvc/ui/popup-menu */ 62),
+ __webpack_require__(/*! mvc/base-mvc */ 5),
+ __webpack_require__(/*! utils/localization */ 7),
+ __webpack_require__(/*! ui/editable-text */ 94),
+ ], __WEBPACK_AMD_DEFINE_RESULT__ = function(
+ HISTORY_VIEW,
+ HISTORY_CONTENTS,
+ STATES,
+ HDA_MODEL,
+ HDA_LI_EDIT,
+ HDCA_LI_EDIT,
+ TAGS,
+ ANNOTATIONS,
+ LIST_COLLECTION_CREATOR,
+ PAIR_COLLECTION_CREATOR,
+ LIST_OF_PAIRS_COLLECTION_CREATOR,
+ faIconButton,
+ PopupMenu,
+ BASE_MVC,
+ _l
+ ){
+
+ 'use strict';
+
+ /* =============================================================================
+ TODO:
+
+ ============================================================================= */
+ var _super = HISTORY_VIEW.HistoryView;
+ // base class for history-view-edit-current and used as-is in history/view.mako
+ /** @class Editable View/Controller for the history model.
+ *
+ * Allows:
+ * (everything HistoryView allows)
+ * changing the name
+ * displaying and editing tags and annotations
+ * multi-selection and operations on mulitple content items
+ */
+ var HistoryViewEdit = _super.extend(
+ /** @lends HistoryViewEdit.prototype */{
+
+ /** logger used to record this.log messages, commonly set to console */
+ //logger : console,
+
+ /** class to use for constructing the HistoryDatasetAssociation views */
+ HDAViewClass : HDA_LI_EDIT.HDAListItemEdit,
+ /** class to use for constructing the HistoryDatasetCollectionAssociation views */
+ HDCAViewClass : HDCA_LI_EDIT.HDCAListItemEdit,
+
+ // ......................................................................... SET UP
+ /** Set up the view, set up storage, bind listeners to HistoryContents events
+ * @param {Object} attributes
+ */
+ initialize : function( attributes ){
+ attributes = attributes || {};
+ _super.prototype.initialize.call( this, attributes );
+
+ // ---- set up instance vars
+ /** editor for tags - sub-view */
+ this.tagsEditor = null;
+ /** editor for annotations - sub-view */
+ this.annotationEditor = null;
+
+ /** allow user purge of dataset files? */
+ this.purgeAllowed = attributes.purgeAllowed || false;
+
+ // states/modes the panel can be in
+ /** is the panel currently showing the dataset selection controls? */
+ this.annotationEditorShown = attributes.annotationEditorShown || false;
+ this.tagsEditorShown = attributes.tagsEditorShown || false;
+ },
+
+ /** Override to handle history as drag-drop target */
+ _setUpListeners : function(){
+ var panel = this;
+ _super.prototype._setUpListeners.call( panel );
+
+ panel.on( 'drop', function( ev, data ){
+ panel.dataDropped( data );
+ // remove the drop target
+ panel.dropTargetOff();
+ });
+ panel.on( 'view:attached view:removed', function(){
+ panel._renderCounts();
+ });
+ },
+
+ // ------------------------------------------------------------------------ listeners
+ /** listening for history and HDA events */
+ _setUpModelListeners : function(){
+ _super.prototype._setUpModelListeners.call( this );
+ this.listenTo( this.model, 'change:size', this.updateHistoryDiskSize );
+ return this;
+ },
+
+ /** listening for collection events */
+ _setUpCollectionListeners : function(){
+ _super.prototype._setUpCollectionListeners.call( this );
+ this.listenTo( this.collection, {
+ 'change:deleted': this._handleHdaDeletionChange,
+ 'change:visible': this._handleHdaVisibleChange,
+ 'change:purged' : function( model ){
+ // hafta get the new nice-size w/o the purged model
+ this.model.fetch();
+ },
+ // loading indicators for deleted/hidden
+ 'fetching-deleted' : function( collection ){
+ this.$( '> .controls .toggle-deleted-link' ).parent()
+ .html( '' + _l( 'loading deleted...' ) + '' );
+ },
+ 'fetching-hidden' : function( collection ){
+ this.$( '> .controls .toggle-hidden-link' ).parent()
+ .html( '' + _l( 'loading hidden...' ) + '' );
+ },
+ 'fetching-deleted-done fetching-hidden-done' : this._renderCounts,
+ });
+ return this;
+ },
+
+ // ------------------------------------------------------------------------ panel rendering
+ /** In this override, add tag and annotation editors and a btn to toggle the selectors */
+ _buildNewRender : function(){
+ // create a new render using a skeleton template, render title buttons, render body, and set up events, etc.
+ var $newRender = _super.prototype._buildNewRender.call( this );
+ if( !this.model ){ return $newRender; }
+
+ if( Galaxy && Galaxy.user && Galaxy.user.id && Galaxy.user.id === this.model.get( 'user_id' ) ){
+ this._renderTags( $newRender );
+ this._renderAnnotation( $newRender );
+ }
+ return $newRender;
+ },
+
+ /** override to render counts when the items are rendered */
+ renderItems : function( $whereTo ){
+ var views = _super.prototype.renderItems.call( this, $whereTo );
+ this._renderCounts( $whereTo );
+ return views;
+ },
+
+ /** override to show counts, what's deleted/hidden, and links to toggle those */
+ _renderCounts : function( $whereTo ){
+ $whereTo = $whereTo instanceof jQuery? $whereTo : this.$el;
+ var html = this.templates.counts( this.model.toJSON(), this );
+ return $whereTo.find( '> .controls .subtitle' ).html( html );
+ },
+
+ /** render the tags sub-view controller */
+ _renderTags : function( $where ){
+ var panel = this;
+ this.tagsEditor = new TAGS.TagsEditor({
+ model : this.model,
+ el : $where.find( '.controls .tags-display' ),
+ onshowFirstTime : function(){ this.render(); },
+ // show hide sub-view tag editors when this is shown/hidden
+ onshow : function(){
+ panel.toggleHDATagEditors( true, panel.fxSpeed );
+ },
+ onhide : function(){
+ panel.toggleHDATagEditors( false, panel.fxSpeed );
+ },
+ $activator : faIconButton({
+ title : _l( 'Edit history tags' ),
+ classes : 'history-tag-btn',
+ faIcon : 'fa-tags'
+ }).appendTo( $where.find( '.controls .actions' ) )
+ });
+ },
+ /** render the annotation sub-view controller */
+ _renderAnnotation : function( $where ){
+ var panel = this;
+ this.annotationEditor = new ANNOTATIONS.AnnotationEditor({
+ model : this.model,
+ el : $where.find( '.controls .annotation-display' ),
+ onshowFirstTime : function(){ this.render(); },
+ // show hide sub-view view annotation editors when this is shown/hidden
+ onshow : function(){
+ panel.toggleHDAAnnotationEditors( true, panel.fxSpeed );
+ },
+ onhide : function(){
+ panel.toggleHDAAnnotationEditors( false, panel.fxSpeed );
+ },
+ $activator : faIconButton({
+ title : _l( 'Edit history annotation' ),
+ classes : 'history-annotate-btn',
+ faIcon : 'fa-comment'
+ }).appendTo( $where.find( '.controls .actions' ) )
+ });
+ },
+
+ /** Set up HistoryViewEdit js/widget behaviours
+ * In this override, make the name editable
+ */
+ _setUpBehaviors : function( $where ){
+ $where = $where || this.$el;
+ _super.prototype._setUpBehaviors.call( this, $where );
+ if( !this.model ){ return; }
+
+ // anon users shouldn't have access to any of the following
+ if( ( !Galaxy.user || Galaxy.user.isAnonymous() )
+ || ( Galaxy.user.id !== this.model.get( 'user_id' ) ) ){
+ return;
+ }
+
+ var panel = this,
+ nameSelector = '> .controls .name';
+ $where.find( nameSelector )
+ .attr( 'title', _l( 'Click to rename history' ) )
+ .tooltip({ placement: 'bottom' })
+ .make_text_editable({
+ on_finish: function( newName ){
+ var previousName = panel.model.get( 'name' );
+ if( newName && newName !== previousName ){
+ panel.$el.find( nameSelector ).text( newName );
+ panel.model.save({ name: newName })
+ .fail( function(){
+ panel.$el.find( nameSelector ).text( panel.model.previous( 'name' ) );
+ });
+ } else {
+ panel.$el.find( nameSelector ).text( previousName );
+ }
+ }
+ });
+ },
+
+ /** return a new popup menu for choosing a multi selection action
+ * ajax calls made for multiple datasets are queued
+ */
+ multiselectActions : function(){
+ var panel = this,
+ actions = [
+ { html: _l( 'Hide datasets' ), func: function(){
+ var action = HDA_MODEL.HistoryDatasetAssociation.prototype.hide;
+ panel.getSelectedModels().ajaxQueue( action );
+ }
+ },
+ { html: _l( 'Unhide datasets' ), func: function(){
+ var action = HDA_MODEL.HistoryDatasetAssociation.prototype.unhide;
+ panel.getSelectedModels().ajaxQueue( action );
+ }
+ },
+ { html: _l( 'Delete datasets' ), func: function(){
+ var action = HDA_MODEL.HistoryDatasetAssociation.prototype['delete'];
+ panel.getSelectedModels().ajaxQueue( action );
+ }
+ },
+ { html: _l( 'Undelete datasets' ), func: function(){
+ var action = HDA_MODEL.HistoryDatasetAssociation.prototype.undelete;
+ panel.getSelectedModels().ajaxQueue( action );
+ }
+ }
+ ];
+ if( panel.purgeAllowed ){
+ actions.push({
+ html: _l( 'Permanently delete datasets' ), func: function(){
+ if( confirm( _l( 'This will permanently remove the data in your datasets. Are you sure?' ) ) ){
+ var action = HDA_MODEL.HistoryDatasetAssociation.prototype.purge;
+ panel.getSelectedModels().ajaxQueue( action );
+ }
+ }
+ });
+ }
+ actions = actions.concat( panel._collectionActions() );
+ return actions;
+ },
+
+ /** */
+ _collectionActions : function(){
+ var panel = this;
+ return [
+ { html: _l( 'Build Dataset List' ), func: function() {
+ LIST_COLLECTION_CREATOR.createListCollection( panel.getSelectedModels() )
+ .done( function(){ panel.model.refresh(); });
+ }
+ },
+ // TODO: Only show quick pair if two things selected.
+ { html: _l( 'Build Dataset Pair' ), func: function() {
+ PAIR_COLLECTION_CREATOR.createPairCollection( panel.getSelectedModels() )
+ .done( function(){ panel.model.refresh(); });
+ }
+ },
+ { html: _l( 'Build List of Dataset Pairs' ), func: function() {
+ LIST_OF_PAIRS_COLLECTION_CREATOR.createListOfPairsCollection( panel.getSelectedModels() )
+ .done( function(){ panel.model.refresh(); });
+ }
+ },
+ ];
+ },
+
+ // ------------------------------------------------------------------------ sub-views
+ /** In this override, add purgeAllowed and whether tags/annotation editors should be shown */
+ _getItemViewOptions : function( model ){
+ var options = _super.prototype._getItemViewOptions.call( this, model );
+ _.extend( options, {
+ purgeAllowed : this.purgeAllowed,
+ //TODO: not working
+ tagsEditorShown : ( this.tagsEditor && !this.tagsEditor.hidden ),
+ annotationEditorShown : ( this.annotationEditor && !this.annotationEditor.hidden )
+ });
+ return options;
+ },
+
+ ///** Override to alter data in drag based on multiselection */
+ //_setUpItemViewListeners : function( view ){
+ // var panel = this;
+ // _super.prototype._setUpItemViewListeners.call( panel, view );
+ //
+ //},
+
+ /** If this item is deleted and we're not showing deleted items, remove the view
+ * @param {Model} the item model to check
+ */
+ _handleHdaDeletionChange : function( itemModel ){
+ var contentsShown = this.model.get( 'contents_shown' );
+ if( itemModel.get( 'deleted' ) ){
+ contentsShown.deleted += 1;
+ if( !this.showDeleted ){
+ this.removeItemView( itemModel );
+ } else {
+ contentsShown.shown -= 1;
+ }
+ } else {
+ contentsShown.deleted -= 1;
+ if( !this.showDeleted ){
+ contentsShown.shown -= 1;
+ }
+ }
+ this.model.set( 'contents_shown', contentsShown );
+ this._renderCounts();
+ },
+
+ /** If this item is hidden and we're not showing hidden items, remove the view
+ * @param {Model} the item model to check
+ */
+ _handleHdaVisibleChange : function( itemModel ){
+ var contentsShown = this.model.get( 'contents_shown' );
+ // console.log( '_handleHdaVisibleChange:', contentsShown, itemModel.hidden(), this.showHidden );
+ if( itemModel.hidden() ){
+ contentsShown.hidden += 1;
+ if( !this.showHidden ){
+ this.removeItemView( itemModel );
+ } else {
+ contentsShown.shown -= 1;
+ }
+ } else {
+ contentsShown.hidden -= 1;
+ if( !this.showHidden ){
+ contentsShown.shown -= 1;
+ }
+ }
+ // console.log( 'contentsShown:', contentsShown.shown, contentsShown.hidden );
+ this.model.set( 'contents_shown', contentsShown );
+ this._renderCounts();
+ },
+
+ /** toggle the visibility of each content's tagsEditor applying all the args sent to this function */
+ toggleHDATagEditors : function( showOrHide ){
+ var args = Array.prototype.slice.call( arguments, 1 );
+ _.each( this.views, function( view ){
+ if( view.tagsEditor ){
+ view.tagsEditor.toggle.apply( view.tagsEditor, args );
+ }
+ });
+ },
+
+ /** toggle the visibility of each content's annotationEditor applying all the args sent to this function */
+ toggleHDAAnnotationEditors : function( showOrHide ){
+ var args = Array.prototype.slice.call( arguments, 1 );
+ _.each( this.views, function( view ){
+ if( view.annotationEditor ){
+ view.annotationEditor.toggle.apply( view.annotationEditor, args );
+ }
+ });
+ },
+
+ // ------------------------------------------------------------------------ panel events
+ /** event map */
+ events : _.extend( _.clone( _super.prototype.events ), {
+ 'click .show-selectors-btn' : 'toggleSelectors',
+ 'click .toggle-deleted-link' : function( ev ){ this.toggleShowDeleted(); },
+ 'click .toggle-hidden-link' : function( ev ){ this.toggleShowHidden(); }
+ }),
+
+ /** Update the history size display (curr. upper right of panel).
+ */
+ updateHistoryDiskSize : function(){
+ this.$( '.history-size' ).text( this.model.get( 'nice_size' ) );
+ },
+
+ // ------------------------------------------------------------------------ as drop target
+ /** */
+ dropTargetOn : function(){
+ if( this.dropTarget ){ return this; }
+ this.dropTarget = true;
+
+ //TODO: to init
+ var dropHandlers = {
+ 'dragenter' : _.bind( this.dragenter, this ),
+ 'dragover' : _.bind( this.dragover, this ),
+ 'dragleave' : _.bind( this.dragleave, this ),
+ 'drop' : _.bind( this.drop, this )
+ };
+ //TODO: scroll to top
+ var $dropTarget = this._renderDropTarget();
+ this.$list().before([ this._renderDropTargetHelp(), $dropTarget ]);
+ for( var evName in dropHandlers ){
+ if( dropHandlers.hasOwnProperty( evName ) ){
+ //console.debug( evName, dropHandlers[ evName ] );
+ $dropTarget.on( evName, dropHandlers[ evName ] );
+ }
+ }
+ return this;
+ },
+
+ /** */
+ _renderDropTarget : function(){
+ this.$( '.history-drop-target' ).remove();
+ return $( '' ).addClass( 'history-drop-target' )
+ .css({
+ 'height': '64px',
+ 'margin': '0px 10px 10px 10px',
+ 'border': '1px dashed black',
+ 'border-radius' : '3px'
+ });
+ },
+
+ /** */
+ _renderDropTargetHelp : function(){
+ this.$( '.history-drop-target-help' ).remove();
+ return $( '' ).addClass( 'history-drop-target-help' )
+ .css({
+ 'margin' : '10px 10px 4px 10px',
+ 'color' : 'grey',
+ 'font-size' : '80%',
+ 'font-style' : 'italic'
+ })
+ .text( _l( 'Drag datasets here to copy them to the current history' ) );
+ },
+
+ /** */
+ dropTargetOff : function(){
+ if( !this.dropTarget ){ return this; }
+ //this.log( 'dropTargetOff' );
+ this.dropTarget = false;
+ var dropTarget = this.$( '.history-drop-target' ).get(0);
+ for( var evName in this._dropHandlers ){
+ if( this._dropHandlers.hasOwnProperty( evName ) ){
+ dropTarget.off( evName, this._dropHandlers[ evName ] );
+ }
+ }
+ this.$( '.history-drop-target' ).remove();
+ this.$( '.history-drop-target-help' ).remove();
+ return this;
+ },
+ /** */
+ dropTargetToggle : function(){
+ if( this.dropTarget ){
+ this.dropTargetOff();
+ } else {
+ this.dropTargetOn();
+ }
+ return this;
+ },
+
+ /** */
+ dragenter : function( ev ){
+ //console.debug( 'dragenter:', this, ev );
+ ev.preventDefault();
+ ev.stopPropagation();
+ this.$( '.history-drop-target' ).css( 'border', '2px solid black' );
+ },
+ /** */
+ dragover : function( ev ){
+ ev.preventDefault();
+ ev.stopPropagation();
+ },
+ /** */
+ dragleave : function( ev ){
+ //console.debug( 'dragleave:', this, ev );
+ ev.preventDefault();
+ ev.stopPropagation();
+ this.$( '.history-drop-target' ).css( 'border', '1px dashed black' );
+ },
+ /** */
+ drop : function( ev ){
+ ev.preventDefault();
+ //ev.stopPropagation();
+
+ var dataTransfer = ev.originalEvent.dataTransfer;
+ dataTransfer.dropEffect = 'move';
+
+ var panel = this,
+ data = dataTransfer.getData( "text" );
+ try {
+ data = JSON.parse( data );
+
+ } catch( err ){
+ this.warn( 'error parsing JSON from drop:', data );
+ }
+ this.trigger( 'droptarget:drop', ev, data, panel );
+ return false;
+ },
+
+ /** */
+ dataDropped : function( data ){
+ var panel = this;
+ // HDA: dropping will copy it to the history
+ if( _.isObject( data ) && data.model_class === 'HistoryDatasetAssociation' && data.id ){
+ return panel.model.contents.copy( data.id );
+ }
+ return jQuery.when();
+ },
+
+ // ........................................................................ misc
+ /** Return a string rep of the history */
+ toString : function(){
+ return 'HistoryViewEdit(' + (( this.model )?( this.model.get( 'name' )):( '' )) + ')';
+ }
+ });
+
+ //------------------------------------------------------------------------------ TEMPLATES
+ HistoryViewEdit.prototype.templates = (function(){
+
+ var countsTemplate = BASE_MVC.wrapTemplate([
+ '<% var shown = Math.max( view.views.length, history.contents_shown.shown ) %>',
+ '<% if( shown ){ %><%- shown %> ', _l( 'shown' ), '<% } %>',
+ '<% if( history.contents_shown.deleted ){ %>',
+ '<% if( view.showDeleted ){ %>',
+ ', ',
+ _l( 'hide deleted' ),
+ '',
+ '<% } else { %>',
+ ', <%- history.contents_shown.deleted %> ',
+ _l( 'deleted' ),
+ '',
+ '<% } %>',
+ '<% } else { %>',
+ '',
+ '<% } %>',
+ '<% if( history.contents_shown.hidden ){ %>',
+ '<% if( view.showHidden ){ %>',
+ ', ',
+ _l( 'hide hidden' ),
+ '',
+ '<% } else { %>',
+ ', <%- history.contents_shown.hidden %> ',
+ _l( 'hidden' ),
+ '',
+ '<% } %>',
+ '<% } else { %>',
+ '',
+ '<% } %>',
+ ], 'history' );
+
+ return _.extend( _.clone( _super.prototype.templates ), {
+ counts : countsTemplate
+ });
+ }());
+
+
+ //==============================================================================
+ return {
+ HistoryViewEdit : HistoryViewEdit
+ };
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! jquery */ 3), __webpack_require__(/*! underscore */ 1), __webpack_require__(/*! jquery */ 3)))
+
+/***/ },
+/* 76 */
+/*!******************************************************************!*\
+ !*** ./galaxy/scripts/mvc/collection/list-collection-creator.js ***!
+ \******************************************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(Backbone, _, $, jQuery) {
+ !(__WEBPACK_AMD_DEFINE_ARRAY__ = [
+ __webpack_require__(/*! mvc/history/hdca-model */ 72),
+ __webpack_require__(/*! mvc/dataset/states */ 70),
+ __webpack_require__(/*! mvc/base-mvc */ 5),
+ __webpack_require__(/*! mvc/ui/ui-modal */ 17),
+ __webpack_require__(/*! utils/natural-sort */ 77),
+ __webpack_require__(/*! utils/localization */ 7),
+ __webpack_require__(/*! ui/hoverhighlight */ 78)
+ ], __WEBPACK_AMD_DEFINE_RESULT__ = function( HDCA, STATES, BASE_MVC, UI_MODAL, naturalSort, _l ){
+
+ 'use strict';
+
+ var logNamespace = 'collections';
+ /*==============================================================================
+ TODO:
+ use proper Element model and not just json
+ straighten out createFn, collection.createHDCA
+ possibly stop using modals for this
+ It would be neat to do a drag and drop
+
+ ==============================================================================*/
+ /** A view for both DatasetDCEs and NestedDCDCEs
+ * (things that implement collection-model:DatasetCollectionElementMixin)
+ */
+ var DatasetCollectionElementView = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend({
+ _logNamespace : logNamespace,
+
+ //TODO: use proper class (DatasetDCE or NestedDCDCE (or the union of both))
+ tagName : 'li',
+ className : 'collection-element',
+
+ initialize : function( attributes ){
+ this.element = attributes.element || {};
+ this.selected = attributes.selected || false;
+ },
+
+ render : function(){
+ this.$el
+ .attr( 'data-element-id', this.element.id )
+ .attr( 'draggable', true )
+ .html( this.template({ element: this.element }) );
+ if( this.selected ){
+ this.$el.addClass( 'selected' );
+ }
+ return this;
+ },
+
+ //TODO: lots of unused space in the element - possibly load details and display them horiz.
+ template : _.template([
+ '',
+ '<%- element.name %>',
+ '',
+ '',
+ ].join('')),
+
+ /** select this element and pub */
+ select : function( toggle ){
+ this.$el.toggleClass( 'selected', toggle );
+ this.trigger( 'select', {
+ source : this,
+ selected : this.$el.hasClass( 'selected' )
+ });
+ },
+
+ /** animate the removal of this element and pub */
+ discard : function(){
+ var view = this,
+ parentWidth = this.$el.parent().width();
+ this.$el.animate({ 'margin-right' : parentWidth }, 'fast', function(){
+ view.trigger( 'discard', {
+ source : view
+ });
+ view.destroy();
+ });
+ },
+
+ /** remove the DOM and any listeners */
+ destroy : function(){
+ this.off();
+ this.$el.remove();
+ },
+
+ events : {
+ 'click' : '_click',
+ 'click .name' : '_clickName',
+ 'click .discard': '_clickDiscard',
+
+ 'dragstart' : '_dragstart',
+ 'dragend' : '_dragend',
+ 'dragover' : '_sendToParent',
+ 'drop' : '_sendToParent'
+ },
+
+ /** select when the li is clicked */
+ _click : function( ev ){
+ ev.stopPropagation();
+ this.select( ev );
+ },
+
+ /** rename a pair when the name is clicked */
+ _clickName : function( ev ){
+ ev.stopPropagation();
+ ev.preventDefault();
+ var promptString = [ _l( 'Enter a new name for the element' ), ':\n(',
+ _l( 'Note that changing the name here will not rename the dataset' ), ')' ].join( '' ),
+ response = prompt( _l( 'Enter a new name for the element' ) + ':', this.element.name );
+ if( response ){
+ this.element.name = response;
+ this.render();
+ }
+ //TODO: cancelling with ESC leads to closure of the creator...
+ },
+
+ /** discard when the discard button is clicked */
+ _clickDiscard : function( ev ){
+ ev.stopPropagation();
+ this.discard();
+ },
+
+ /** dragging pairs for re-ordering */
+ _dragstart : function( ev ){
+ if( ev.originalEvent ){ ev = ev.originalEvent; }
+ ev.dataTransfer.effectAllowed = 'move';
+ ev.dataTransfer.setData( 'text/plain', JSON.stringify( this.element ) );
+
+ this.$el.addClass( 'dragging' );
+ this.$el.parent().trigger( 'collection-element.dragstart', [ this ] );
+ },
+
+ /** dragging for re-ordering */
+ _dragend : function( ev ){
+ this.$el.removeClass( 'dragging' );
+ this.$el.parent().trigger( 'collection-element.dragend', [ this ] );
+ },
+
+ /** manually bubble up an event to the parent/container */
+ _sendToParent : function( ev ){
+ this.$el.parent().trigger( ev );
+ },
+
+ /** string rep */
+ toString : function(){
+ return 'DatasetCollectionElementView()';
+ }
+ });
+
+
+ // ============================================================================
+ /** An interface for building collections.
+ */
+ var ListCollectionCreator = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend({
+ _logNamespace : logNamespace,
+
+ /** the class used to display individual elements */
+ elementViewClass : DatasetCollectionElementView,
+ /** the class this creator will create and save */
+ collectionClass : HDCA.HistoryListDatasetCollection,
+ className : 'list-collection-creator collection-creator flex-row-container',
+
+ /** minimum number of valid elements to start with in order to build a collection of this type */
+ minElements : 1,
+
+ defaultAttributes : {
+ //TODO: remove - use new collectionClass().save()
+ /** takes elements and creates the proper collection - returns a promise */
+ creationFn : function(){ throw new TypeError( 'no creation fn for creator' ); },
+ /** fn to call when the collection is created (scoped to this) */
+ oncreate : function(){},
+ /** fn to call when the cancel button is clicked (scoped to this) - if falsy, no btn is displayed */
+ oncancel : function(){},
+ /** distance from list edge to begin autoscrolling list */
+ autoscrollDist : 24,
+ /** Color passed to hoverhighlight */
+ highlightClr : 'rgba( 64, 255, 255, 1.0 )'
+ },
+
+ /** set up initial options, instance vars, behaviors */
+ initialize : function( attributes ){
+ this.metric( 'ListCollectionCreator.initialize', attributes );
+ var creator = this;
+ _.each( this.defaultAttributes, function( value, key ){
+ value = attributes[ key ] || value;
+ creator[ key ] = value;
+ });
+
+ /** unordered, original list - cache to allow reversal */
+ creator.initialElements = attributes.elements || [];
+
+ this._instanceSetUp();
+ this._elementsSetUp();
+ this._setUpBehaviors();
+ },
+
+ /** set up instance vars */
+ _instanceSetUp : function(){
+ /** Ids of elements that have been selected by the user - to preserve over renders */
+ this.selectedIds = {};
+ /** DOM elements currently being dragged */
+ this.$dragging = null;
+ /** Used for blocking UI events during ajax/operations (don't post twice) */
+ this.blocking = false;
+ },
+
+ // ------------------------------------------------------------------------ process raw list
+ /** set up main data */
+ _elementsSetUp : function(){
+ //this.debug( '-- _dataSetUp' );
+ /** a list of invalid elements and the reasons they aren't valid */
+ this.invalidElements = [];
+ //TODO: handle fundamental problem of syncing DOM, views, and list here
+ /** data for list in progress */
+ this.workingElements = [];
+ /** views for workingElements */
+ this.elementViews = [];
+
+ // copy initial list, sort, add ids if needed
+ this.workingElements = this.initialElements.slice( 0 );
+ this._ensureElementIds();
+ this._validateElements();
+ this._mangleDuplicateNames();
+ this._sortElements();
+ },
+
+ /** add ids to dataset objs in initial list if none */
+ _ensureElementIds : function(){
+ this.workingElements.forEach( function( element ){
+ if( !element.hasOwnProperty( 'id' ) ){
+ element.id = _.uniqueId();
+ }
+ });
+ return this.workingElements;
+ },
+
+ /** separate working list into valid and invalid elements for this collection */
+ _validateElements : function(){
+ var creator = this,
+ existingNames = {};
+ creator.invalidElements = [];
+
+ this.workingElements = this.workingElements.filter( function( element ){
+ var problem = creator._isElementInvalid( element );
+ if( problem ){
+ creator.invalidElements.push({
+ element : element,
+ text : problem
+ });
+ }
+ return !problem;
+ });
+ return this.workingElements;
+ },
+
+ /** describe what is wrong with a particular element if anything */
+ _isElementInvalid : function( element ){
+ if( element.history_content_type !== 'dataset' ){
+ return _l( "is not a dataset" );
+ }
+ if( element.state !== STATES.OK ){
+ if( _.contains( STATES.NOT_READY_STATES, element.state ) ){
+ return _l( "hasn't finished running yet" );
+ }
+ return _l( "has errored, is paused, or is not accessible" );
+ }
+ if( element.deleted || element.purged ){
+ return _l( "has been deleted or purged" );
+ }
+ return null;
+ },
+
+ /** mangle duplicate names using a mac-like '(counter)' addition to any duplicates */
+ _mangleDuplicateNames : function(){
+ var SAFETY = 900,
+ counter = 1,
+ existingNames = {};
+ this.workingElements.forEach( function( element ){
+ var currName = element.name;
+ while( existingNames.hasOwnProperty( currName ) ){
+ currName = element.name + ' (' + counter + ')';
+ counter += 1;
+ if( counter >= SAFETY ){
+ throw new Error( 'Safety hit in while loop - thats impressive' );
+ }
+ }
+ element.name = currName;
+ existingNames[ element.name ] = true;
+ });
+ },
+
+ /** sort a list of elements */
+ _sortElements : function( list ){
+ // // currently only natural sort by name
+ // this.workingElements.sort( function( a, b ){ return naturalSort( a.name, b.name ); });
+ // return this.workingElements;
+ },
+
+ // ------------------------------------------------------------------------ rendering
+ // templates : ListCollectionCreator.templates,
+ /** render the entire interface */
+ render : function( speed, callback ){
+ //this.debug( '-- _render' );
+ if( this.workingElements.length < this.minElements ){
+ return this._renderInvalid( speed, callback );
+ }
+
+ this.$el.empty().html( this.templates.main() );
+ this._renderHeader( speed );
+ this._renderMiddle( speed );
+ this._renderFooter( speed );
+ this._addPluginComponents();
+ this.$( '.collection-name' ).focus();
+ this.trigger( 'rendered', this );
+ return this;
+ },
+
+
+ /** render a simplified interface aimed at telling the user why they can't move forward */
+ _renderInvalid : function( speed, callback ){
+ //this.debug( '-- _render' );
+ this.$el.empty().html( this.templates.invalidInitial({
+ problems: this.invalidElements,
+ elements: this.workingElements,
+ }));
+ if( typeof this.oncancel === 'function' ){
+ this.$( '.cancel-create.btn' ).show();
+ }
+ this.trigger( 'rendered', this );
+ return this;
+ },
+
+ /** render the header section */
+ _renderHeader : function( speed, callback ){
+ var $header = this.$( '.header' ).empty().html( this.templates.header() )
+ .find( '.help-content' ).prepend( $( this.templates.helpContent() ) );
+ //TODO: should only show once despite calling _renderHeader again
+ if( this.invalidElements.length ){
+ this._invalidElementsAlert();
+ }
+ return $header;
+ },
+
+ /** render the middle including the elements */
+ _renderMiddle : function( speed, callback ){
+ var $middle = this.$( '.middle' ).empty().html( this.templates.middle() );
+ this._renderList( speed );
+ return $middle;
+ },
+
+ /** render the footer, completion controls, and cancel controls */
+ _renderFooter : function( speed, callback ){
+ var $footer = this.$( '.footer' ).empty().html( this.templates.footer() );
+ if( typeof this.oncancel === 'function' ){
+ this.$( '.cancel-create.btn' ).show();
+ }
+ return $footer;
+ },
+
+ /** add any jQuery/bootstrap/custom plugins to elements rendered */
+ _addPluginComponents : function(){
+ this.$( '.help-content i' ).hoverhighlight( '.collection-creator', this.highlightClr );
+ },
+
+ /** build and show an alert describing any elements that could not be included due to problems */
+ _invalidElementsAlert : function(){
+ this._showAlert( this.templates.invalidElements({ problems: this.invalidElements }), 'alert-warning' );
+ },
+
+ /** add (or clear if clear is truthy) a validation warning to the DOM element described in what */
+ _validationWarning : function( what, clear ){
+ var VALIDATION_CLASS = 'validation-warning';
+ if( what === 'name' ){
+ what = this.$( '.collection-name' ).add( this.$( '.collection-name-prompt' ) );
+ this.$( '.collection-name' ).focus().select();
+ }
+ if( clear ){
+ what = what || this.$( '.' + VALIDATION_CLASS );
+ what.removeClass( VALIDATION_CLASS );
+ } else {
+ what.addClass( VALIDATION_CLASS );
+ }
+ },
+
+ _disableNameAndCreate : function( disable ){
+ disable = !_.isUndefined( disable )? disable : true;
+ if( disable ){
+ this.$( '.collection-name' ).prop( 'disabled', true );
+ this.$( '.create-collection' ).toggleClass( 'disabled', true );
+ // } else {
+ // this.$( '.collection-name' ).prop( 'disabled', false );
+ // this.$( '.create-collection' ).removeClass( 'disable' );
+ }
+ },
+
+ // ------------------------------------------------------------------------ rendering elements
+ /** conv. to the main list display DOM */
+ $list : function(){
+ return this.$( '.collection-elements' );
+ },
+
+ /** show or hide the clear selected control based on the num of selected elements */
+ _renderClearSelected : function(){
+ if( _.size( this.selectedIds ) ){
+ this.$( '.collection-elements-controls > .clear-selected' ).show();
+ } else {
+ this.$( '.collection-elements-controls > .clear-selected' ).hide();
+ }
+ },
+
+ /** render the elements in order (or a warning if no elements found) */
+ _renderList : function( speed, callback ){
+ //this.debug( '-- _renderList' );
+ var creator = this,
+ $tmp = jQuery( '' ),
+ $list = creator.$list();
+
+ _.each( this.elementViews, function( view ){
+ view.destroy();
+ creator.removeElementView( view );
+ });
+
+ // if( !this.workingElements.length ){
+ // this._renderNoValidElements();
+ // return;
+ // }
+
+ creator.workingElements.forEach( function( element ){
+ var elementView = creator._createElementView( element );
+ $tmp.append( elementView.$el );
+ });
+
+ creator._renderClearSelected();
+ $list.empty().append( $tmp.children() );
+ _.invoke( creator.elementViews, 'render' );
+
+ if( $list.height() > $list.css( 'max-height' ) ){
+ $list.css( 'border-width', '1px 0px 1px 0px' );
+ } else {
+ $list.css( 'border-width', '0px' );
+ }
+ },
+
+ /** create an element view, cache in elementViews, set up listeners, and return */
+ _createElementView : function( element ){
+ var elementView = new this.elementViewClass({
+ //TODO: use non-generic class or not all
+ // model : COLLECTION.DatasetDCE( element )
+ element : element,
+ selected: _.has( this.selectedIds, element.id )
+ });
+ this.elementViews.push( elementView );
+ this._listenToElementView( elementView );
+ return elementView;
+ },
+
+ /** listen to any element events */
+ _listenToElementView : function( view ){
+ var creator = this;
+ creator.listenTo( view, {
+ select : function( data ){
+ var element = data.source.element;
+ if( data.selected ){
+ creator.selectedIds[ element.id ] = true;
+ } else {
+ delete creator.selectedIds[ element.id ];
+ }
+ creator.trigger( 'elements:select', data );
+ },
+ discard : function( data ){
+ creator.trigger( 'elements:discard', data );
+ }
+ });
+ },
+
+ /** add a new element view based on the json in element */
+ addElementView : function( element ){
+ //TODO: workingElements is sorted, add element in appropo index
+ // add element, sort elements, find element index
+ // var view = this._createElementView( element );
+ // return view;
+ },
+
+ /** stop listening to view and remove from caches */
+ removeElementView : function( view ){
+ delete this.selectedIds[ view.element.id ];
+ this._renderClearSelected();
+
+ this.elementViews = _.without( this.elementViews, view );
+ this.stopListening( view );
+ },
+
+ /** render a message in the list that no elements remain to create a collection */
+ _renderNoElementsLeft : function(){
+ this._disableNameAndCreate( true );
+ this.$( '.collection-elements' ).append( this.templates.noElementsLeft() );
+ },
+
+ // /** render a message in the list that no valid elements were found to create a collection */
+ // _renderNoValidElements : function(){
+ // this._disableNameAndCreate( true );
+ // this.$( '.collection-elements' ).append( this.templates.noValidElements() );
+ // },
+
+ // ------------------------------------------------------------------------ API
+ /** convert element into JSON compatible with the collections API */
+ _elementToJSON : function( element ){
+ // return element.toJSON();
+ return element;
+ },
+
+ /** create the collection via the API
+ * @returns {jQuery.xhr Object} the jquery ajax request
+ */
+ createList : function( name ){
+ if( !this.workingElements.length ){
+ var message = _l( 'No valid elements for final list' ) + '. ';
+ message += '' + _l( 'Cancel' ) + ' ';
+ message += _l( 'or' );
+ message += ' ' + _l( 'start over' ) + '.';
+ this._showAlert( message );
+ return;
+ }
+
+ var creator = this,
+ elements = this.workingElements.map( function( element ){
+ return creator._elementToJSON( element );
+ });
+
+ creator.blocking = true;
+ return creator.creationFn( elements, name )
+ .always( function(){
+ creator.blocking = false;
+ })
+ .fail( function( xhr, status, message ){
+ creator.trigger( 'error', {
+ xhr : xhr,
+ status : status,
+ message : _l( 'An error occurred while creating this collection' )
+ });
+ })
+ .done( function( response, message, xhr ){
+ creator.trigger( 'collection:created', response, message, xhr );
+ creator.metric( 'collection:created', response );
+ if( typeof creator.oncreate === 'function' ){
+ creator.oncreate.call( this, response, message, xhr );
+ }
+ });
+ },
+
+ // ------------------------------------------------------------------------ events
+ /** set up event handlers on self */
+ _setUpBehaviors : function(){
+ this.on( 'error', this._errorHandler );
+
+ this.once( 'rendered', function(){
+ this.trigger( 'rendered:initial', this );
+ });
+
+ this.on( 'elements:select', function( data ){
+ this._renderClearSelected();
+ });
+
+ this.on( 'elements:discard', function( data ){
+ var element = data.source.element;
+ this.removeElementView( data.source );
+
+ this.workingElements = _.without( this.workingElements, element );
+ if( !this.workingElements.length ){
+ this._renderNoElementsLeft();
+ }
+ });
+
+ //this.on( 'all', function(){
+ // this.info( arguments );
+ //});
+ return this;
+ },
+
+ /** handle errors with feedback and details to the user (if available) */
+ _errorHandler : function( data ){
+ this.error( data );
+
+ var creator = this;
+ content = data.message || _l( 'An error occurred' );
+ if( data.xhr ){
+ var xhr = data.xhr,
+ message = data.message;
+ if( xhr.readyState === 0 && xhr.status === 0 ){
+ content += ': ' + _l( 'Galaxy could not be reached and may be updating.' ) +
+ _l( ' Try again in a few minutes.' );
+ } else if( xhr.responseJSON ){
+ content += ':
' + JSON.stringify( xhr.responseJSON ) + '
';
+ } else {
+ content += ': ' + message;
+ }
+ }
+ creator._showAlert( content, 'alert-danger' );
+ },
+
+ events : {
+ // header
+ 'click .more-help' : '_clickMoreHelp',
+ 'click .less-help' : '_clickLessHelp',
+ 'click .main-help' : '_toggleHelp',
+ 'click .header .alert button' : '_hideAlert',
+
+ 'click .reset' : 'reset',
+ 'click .clear-selected' : 'clearSelectedElements',
+
+ // elements - selection
+ 'click .collection-elements' : 'clearSelectedElements',
+
+ // elements - drop target
+ // 'dragenter .collection-elements': '_dragenterElements',
+ // 'dragleave .collection-elements': '_dragleaveElements',
+ 'dragover .collection-elements' : '_dragoverElements',
+ 'drop .collection-elements' : '_dropElements',
+
+ // these bubble up from the elements as custom events
+ 'collection-element.dragstart .collection-elements' : '_elementDragstart',
+ 'collection-element.dragend .collection-elements' : '_elementDragend',
+
+ // footer
+ 'change .collection-name' : '_changeName',
+ 'keydown .collection-name' : '_nameCheckForEnter',
+ 'click .cancel-create' : function( ev ){
+ if( typeof this.oncancel === 'function' ){
+ this.oncancel.call( this );
+ }
+ },
+ 'click .create-collection' : '_clickCreate'//,
+ },
+
+ // ........................................................................ header
+ /** expand help */
+ _clickMoreHelp : function( ev ){
+ ev.stopPropagation();
+ this.$( '.main-help' ).addClass( 'expanded' );
+ this.$( '.more-help' ).hide();
+ },
+ /** collapse help */
+ _clickLessHelp : function( ev ){
+ ev.stopPropagation();
+ this.$( '.main-help' ).removeClass( 'expanded' );
+ this.$( '.more-help' ).show();
+ },
+ /** toggle help */
+ _toggleHelp : function( ev ){
+ ev.stopPropagation();
+ this.$( '.main-help' ).toggleClass( 'expanded' );
+ this.$( '.more-help' ).toggle();
+ },
+
+ /** show an alert on the top of the interface containing message (alertClass is bootstrap's alert-*) */
+ _showAlert : function( message, alertClass ){
+ alertClass = alertClass || 'alert-danger';
+ this.$( '.main-help' ).hide();
+ this.$( '.header .alert' )
+ .attr( 'class', 'alert alert-dismissable' ).addClass( alertClass ).show()
+ .find( '.alert-message' ).html( message );
+ },
+ /** hide the alerts at the top */
+ _hideAlert : function( message ){
+ this.$( '.main-help' ).show();
+ this.$( '.header .alert' ).hide();
+ },
+
+ // ........................................................................ elements
+ /** reset all data to the initial state */
+ reset : function(){
+ this._instanceSetUp();
+ this._elementsSetUp();
+ this.render();
+ },
+
+ /** deselect all elements */
+ clearSelectedElements : function( ev ){
+ this.$( '.collection-elements .collection-element' ).removeClass( 'selected' );
+ this.$( '.collection-elements-controls > .clear-selected' ).hide();
+ },
+
+ //_dragenterElements : function( ev ){
+ // //this.debug( '_dragenterElements:', ev );
+ //},
+ //TODO: if selected are dragged out of the list area - remove the placeholder - cuz it won't work anyway
+ // _dragleaveElements : function( ev ){
+ // //this.debug( '_dragleaveElements:', ev );
+ // },
+
+ /** track the mouse drag over the list adding a placeholder to show where the drop would occur */
+ _dragoverElements : function( ev ){
+ //this.debug( '_dragoverElements:', ev );
+ ev.preventDefault();
+
+ var $list = this.$list();
+ this._checkForAutoscroll( $list, ev.originalEvent.clientY );
+ var $nearest = this._getNearestElement( ev.originalEvent.clientY );
+
+ //TODO: no need to re-create - move instead
+ this.$( '.element-drop-placeholder' ).remove();
+ var $placeholder = $( '' );
+ if( !$nearest.size() ){
+ $list.append( $placeholder );
+ } else {
+ $nearest.before( $placeholder );
+ }
+ },
+
+ /** If the mouse is near enough to the list's top or bottom, scroll the list */
+ _checkForAutoscroll : function( $element, y ){
+ var AUTOSCROLL_SPEED = 2,
+ offset = $element.offset(),
+ scrollTop = $element.scrollTop(),
+ upperDist = y - offset.top,
+ lowerDist = ( offset.top + $element.outerHeight() ) - y;
+ if( upperDist >= 0 && upperDist < this.autoscrollDist ){
+ $element.scrollTop( scrollTop - AUTOSCROLL_SPEED );
+ } else if( lowerDist >= 0 && lowerDist < this.autoscrollDist ){
+ $element.scrollTop( scrollTop + AUTOSCROLL_SPEED );
+ }
+ },
+
+ /** get the nearest element based on the mouse's Y coordinate.
+ * If the y is at the end of the list, return an empty jQuery object.
+ */
+ _getNearestElement : function( y ){
+ var WIGGLE = 4,
+ lis = this.$( '.collection-elements li.collection-element' ).toArray();
+ for( var i=0; i y && top - halfHeight < y ){
+ return $li;
+ }
+ }
+ return $();
+ },
+
+ /** drop (dragged/selected elements) onto the list, re-ordering the internal list */
+ _dropElements : function( ev ){
+ if( ev.originalEvent ){ ev = ev.originalEvent; }
+ // both required for firefox
+ ev.preventDefault();
+ ev.dataTransfer.dropEffect = 'move';
+
+ // insert before the nearest element or after the last.
+ var $nearest = this._getNearestElement( ev.clientY );
+ if( $nearest.size() ){
+ this.$dragging.insertBefore( $nearest );
+ } else {
+ // no nearest before - insert after last element
+ this.$dragging.insertAfter( this.$( '.collection-elements .collection-element' ).last() );
+ }
+ // resync the creator's list based on the new DOM order
+ this._syncOrderToDom();
+ return false;
+ },
+
+ /** resync the creator's list of elements based on the DOM order */
+ _syncOrderToDom : function(){
+ var creator = this,
+ newElements = [];
+ //TODO: doesn't seem wise to use the dom to store these - can't we sync another way?
+ this.$( '.collection-elements .collection-element' ).each( function(){
+ var id = $( this ).attr( 'data-element-id' ),
+ element = _.findWhere( creator.workingElements, { id: id });
+ if( element ){
+ newElements.push( element );
+ } else {
+ console.error( 'missing element: ', id );
+ }
+ });
+ this.workingElements = newElements;
+ this._renderList();
+ },
+
+ /** drag communication with element sub-views: dragstart */
+ _elementDragstart : function( ev, element ){
+ // auto select the element causing the event and move all selected
+ element.select( true );
+ this.$dragging = this.$( '.collection-elements .collection-element.selected' );
+ },
+
+ /** drag communication with element sub-views: dragend - remove the placeholder */
+ _elementDragend : function( ev, element ){
+ $( '.element-drop-placeholder' ).remove();
+ this.$dragging = null;
+ },
+
+ // ........................................................................ footer
+ /** handle a collection name change */
+ _changeName : function( ev ){
+ this._validationWarning( 'name', !!this._getName() );
+ },
+
+ /** check for enter key press when in the collection name and submit */
+ _nameCheckForEnter : function( ev ){
+ if( ev.keyCode === 13 && !this.blocking ){
+ this._clickCreate();
+ }
+ },
+
+ /** get the current collection name */
+ _getName : function(){
+ return _.escape( this.$( '.collection-name' ).val() );
+ },
+
+ /** attempt to create the current collection */
+ _clickCreate : function( ev ){
+ var name = this._getName();
+ if( !name ){
+ this._validationWarning( 'name' );
+ } else if( !this.blocking ){
+ this.createList( name );
+ }
+ },
+
+ // ------------------------------------------------------------------------ templates
+ //TODO: move to require text plugin and load these as text
+ //TODO: underscore currently unnecc. bc no vars are used
+ //TODO: better way of localizing text-nodes in long strings
+ /** underscore template fns attached to class */
+ templates : {
+ /** the skeleton */
+ main : _.template([
+ '',
+ '',
+ ''
+ ].join('')),
+
+ /** the header (not including help text) */
+ header : _.template([
+ '
', _l([
+ 'Collections of datasets are permanent, ordered lists of datasets that can be passed to tools and ',
+ 'workflows in order to have analyses done on each member of the entire group. This interface allows ',
+ 'you to create a collection and re-order the final collection.'
+ ].join( '' )), '
',
+ '
',
+ '
', _l([
+ 'Rename elements in the list by clicking on ',
+ 'the existing name.'
+ ].join( '' )), '
',
+ '
', _l([
+ 'Discard elements from the final created list by clicking on the ',
+ '"Discard" button.'
+ ].join( '' )), '
',
+ '
', _l([
+ 'Reorder the list by clicking and dragging elements. Select multiple elements by clicking on ',
+ 'them and you can then move those selected by dragging the ',
+ 'entire group. Deselect them by clicking them again or by clicking the ',
+ 'the "Clear selected" link.'
+ ].join( '' )), '
',
+ '
', _l([
+ 'Click the "Start over" link to begin again as if you had just opened ',
+ 'the interface.'
+ ].join( '' )), '
',
+ '
', _l([
+ 'Click the "Cancel" button to exit the interface.'
+ ].join( '' )), '
',
+ '
',
+ '
', _l([
+ 'Once your collection is complete, enter a name and ',
+ 'click "Create list".'
+ ].join( '' )), '
'
+ ].join('')),
+
+ /** shown in list when all elements are discarded */
+ invalidElements : _.template([
+ _l( 'The following selections could not be included due to problems:' ),
+ '
<% _.each( problems, function( problem ){ %>',
+ '
<%- problem.element.name %>: <%- problem.text %>
',
+ '<% }); %>
'
+ ].join('')),
+
+ /** shown in list when all elements are discarded */
+ noElementsLeft : _.template([
+ '
'
+ ].join('')),
+
+ /** a simplified page communicating what went wrong and why the user needs to reselect something else */
+ invalidInitial : _.template([
+ '
',
+ '
',
+ '',
+ '<% if( _.size( problems ) ){ %>',
+ _l( 'The following selections could not be included due to problems' ), ':',
+ '
<% _.each( problems, function( problem ){ %>',
+ '
<%- problem.element.name %>: <%- problem.text %>
',
+ '<% }); %>
',
+ '<% } else if( _.size( elements ) < 1 ){ %>',
+ _l( 'No datasets were selected' ), '.',
+ '<% } %>',
+ ' ',
+ _l( 'At least one element is needed for the collection' ), '. ',
+ _l( 'You may need to ' ),
+ '', _l( 'cancel' ), ' ',
+ _l( 'and reselect new elements' ), '.',
+ '',
+ '
',
+ '
',
+ ''
+ ].join('')),
+ },
+
+ // ------------------------------------------------------------------------ misc
+ /** string rep */
+ toString : function(){ return 'ListCollectionCreator'; }
+ });
+
+
+
+ //=============================================================================
+ /** Create a modal and load its body with the given CreatorClass creator type
+ * @returns {Deferred} resolved when creator has built a collection.
+ */
+ var collectionCreatorModal = function _collectionCreatorModal( elements, options, CreatorClass ){
+
+ var deferred = jQuery.Deferred(),
+ modal = Galaxy.modal || ( new UI_MODAL.View() ),
+ creator;
+
+ options = _.defaults( options || {}, {
+ elements : elements,
+ oncancel : function(){
+ modal.hide();
+ deferred.reject( 'cancelled' );
+ },
+ oncreate : function( creator, response ){
+ modal.hide();
+ deferred.resolve( response );
+ }
+ });
+
+ creator = new CreatorClass( options );
+ modal.show({
+ title : options.title || _l( 'Create a collection' ),
+ body : creator.$el,
+ width : '80%',
+ height : '100%',
+ closing_events: true
+ });
+ creator.render();
+ window._collectionCreator = creator;
+
+ //TODO: remove modal header
+ return deferred;
+ };
+
+ /** List collection flavor of collectionCreatorModal. */
+ var listCollectionCreatorModal = function _listCollectionCreatorModal( elements, options ){
+ options = options || {};
+ options.title = _l( 'Create a collection from a list of datasets' );
+ return collectionCreatorModal( elements, options, ListCollectionCreator );
+ };
+
+
+ //==============================================================================
+ /** Use a modal to create a list collection, then add it to the given history contents.
+ * @returns {Deferred} resolved when the collection is added to the history.
+ */
+ function createListCollection( contents ){
+ var elements = contents.toJSON(),
+ promise = listCollectionCreatorModal( elements, {
+ creationFn : function( elements, name ){
+ elements = elements.map( function( element ){
+ return {
+ id : element.id,
+ name : element.name,
+ //TODO: this allows for list:list even if the filter above does not - reconcile
+ src : ( element.history_content_type === 'dataset'? 'hda' : 'hdca' )
+ };
+ });
+ return contents.createHDCA( elements, 'list', name );
+ }
+ });
+ return promise;
+ }
+
+ //==============================================================================
+ return {
+ DatasetCollectionElementView: DatasetCollectionElementView,
+ ListCollectionCreator : ListCollectionCreator,
+
+ collectionCreatorModal : collectionCreatorModal,
+ listCollectionCreatorModal : listCollectionCreatorModal,
+ createListCollection : createListCollection
+ };
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! libs/backbone */ 2), __webpack_require__(/*! underscore */ 1), __webpack_require__(/*! jquery */ 3), __webpack_require__(/*! jquery */ 3)))
+
+/***/ },
+/* 77 */
+/*!**********************************************!*\
+ !*** ./galaxy/scripts/utils/natural-sort.js ***!
+ \**********************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_RESULT__ = function(){
+ // Alphanumeric/natural sort fn
+ function naturalSort(a, b) {
+ // setup temp-scope variables for comparison evauluation
+ var re = /(-?[0-9\.]+)/g,
+ x = a.toString().toLowerCase() || '',
+ y = b.toString().toLowerCase() || '',
+ nC = String.fromCharCode(0),
+ xN = x.replace( re, nC + '$1' + nC ).split(nC),
+ yN = y.replace( re, nC + '$1' + nC ).split(nC),
+ xD = (new Date(x)).getTime(),
+ yD = xD ? (new Date(y)).getTime() : null;
+ // natural sorting of dates
+ if ( yD ) {
+ if ( xD < yD ) { return -1; }
+ else if ( xD > yD ) { return 1; }
+ }
+ // natural sorting through split numeric strings and default strings
+ var oFxNcL, oFyNcL;
+ for ( var cLoc = 0, numS = Math.max(xN.length, yN.length); cLoc < numS; cLoc++ ) {
+ oFxNcL = parseFloat(xN[cLoc]) || xN[cLoc];
+ oFyNcL = parseFloat(yN[cLoc]) || yN[cLoc];
+ if (oFxNcL < oFyNcL) { return -1; }
+ else if (oFxNcL > oFyNcL) { return 1; }
+ }
+ return 0;
+ }
+
+ return naturalSort;
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__))
+
+
+/***/ },
+/* 78 */
+/*!*********************************************!*\
+ !*** ./galaxy/scripts/ui/hoverhighlight.js ***!
+ \*********************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(jQuery, $) {(function (factory) {
+ if (true) {
+ !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+ } else {
+ // Browser globals
+ factory(jQuery);
+ }
+
+ }(function () {
+ //=============================================================================
+
+ jQuery.fn.extend({
+ hoverhighlight : function $hoverhighlight( scope, color ){
+ scope = scope || 'body';
+ if( !this.size() ){ return this; }
+
+ $( this ).each( function(){
+ var $this = $( this ),
+ targetSelector = $this.data( 'target' );
+
+ if( targetSelector ){
+ $this.mouseover( function( ev ){
+ $( targetSelector, scope ).css({
+ background: color
+ });
+ })
+ .mouseout( function( ev ){
+ $( targetSelector ).css({
+ background: ''
+ });
+ });
+ }
+ });
+ return this;
+ }
+ });
+ }));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! jquery */ 3), __webpack_require__(/*! jquery */ 3)))
+
+/***/ },
+/* 79 */
+/*!****************************************************!*\
+ !*** ./galaxy/scripts/mvc/history/history-view.js ***!
+ \****************************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(_, $, jQuery) {!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
+ __webpack_require__(/*! mvc/list/list-view */ 80),
+ __webpack_require__(/*! mvc/history/history-model */ 65),
+ __webpack_require__(/*! mvc/history/history-contents */ 66),
+ __webpack_require__(/*! mvc/history/history-preferences */ 74),
+ __webpack_require__(/*! mvc/history/hda-li */ 84),
+ __webpack_require__(/*! mvc/history/hdca-li */ 87),
+ __webpack_require__(/*! mvc/user/user-model */ 9),
+ __webpack_require__(/*! ui/fa-icon-button */ 86),
+ __webpack_require__(/*! mvc/base-mvc */ 5),
+ __webpack_require__(/*! utils/localization */ 7),
+ __webpack_require__(/*! ui/search-input */ 83)
+ ], __WEBPACK_AMD_DEFINE_RESULT__ = function(
+ LIST_VIEW,
+ HISTORY_MODEL,
+ HISTORY_CONTENTS,
+ HISTORY_PREFS,
+ HDA_LI,
+ HDCA_LI,
+ USER,
+ faIconButton,
+ BASE_MVC,
+ _l
+ ){
+ 'use strict';
+
+ /* =============================================================================
+ TODO:
+
+ ============================================================================= */
+ /** @class non-editable, read-only View/Controller for a history model.
+ * Allows:
+ * changing the loaded history
+ * displaying data, info, and download
+ * tracking history attrs: size, tags, annotations, name, etc.
+ * Does not allow:
+ * changing the name
+ */
+ var _super = LIST_VIEW.ModelListPanel;
+ var HistoryView = _super.extend(
+ /** @lends HistoryView.prototype */{
+ _logNamespace : 'history',
+
+ /** class to use for constructing the HDA views */
+ HDAViewClass : HDA_LI.HDAListItemView,
+ /** class to use for constructing the HDCA views */
+ HDCAViewClass : HDCA_LI.HDCAListItemView,
+ /** class to used for constructing collection of sub-view models */
+ collectionClass : HISTORY_CONTENTS.HistoryContents,
+ /** key of attribute in model to assign to this.collection */
+ modelCollectionKey : 'contents',
+
+ tagName : 'div',
+ className : _super.prototype.className + ' history-panel',
+
+ /** string to display when the collection is empty */
+ emptyMsg : _l( 'This history is empty' ),
+ /** displayed when no items match the search terms */
+ noneFoundMsg : _l( 'No matching datasets found' ),
+ /** string used for search placeholder */
+ searchPlaceholder : _l( 'search datasets' ),
+
+ // ......................................................................... SET UP
+ /** Set up the view, bind listeners.
+ * @param {Object} attributes optional settings for the panel
+ */
+ initialize : function( attributes ){
+ _super.prototype.initialize.call( this, attributes );
+ // ---- instance vars
+ // control contents/behavior based on where (and in what context) the panel is being used
+ /** where should pages from links be displayed? (default to new tab/window) */
+ this.linkTarget = attributes.linkTarget || '_blank';
+ },
+
+ /** In this override, clear the update timer on the model */
+ freeModel : function(){
+ _super.prototype.freeModel.call( this );
+ if( this.model ){
+ //TODO: move to History.free()
+ this.model.clearUpdateTimeout();
+ }
+ return this;
+ },
+
+ /** create any event listeners for the panel
+ * @fires: rendered:initial on the first render
+ * @fires: empty-history when switching to a history with no contents or creating a new history
+ */
+ _setUpListeners : function(){
+ _super.prototype._setUpListeners.call( this );
+ this.on({
+ error : function( model, xhr, options, msg, details ){
+ this.errorHandler( model, xhr, options, msg, details );
+ },
+ 'loading-done' : function(){
+ this.render();
+ if( !this.views.length ){
+ this.trigger( 'empty-history', this );
+ }
+ },
+ 'views:ready view:attached view:removed' : function( view ){
+ this._renderSelectButton();
+ }
+ });
+ // this.on( 'all', function(){ console.debug( arguments ); });
+ },
+
+ // ------------------------------------------------------------------------ inf. scrolling
+ /** @type {Number} ms to debounce scroll handler for infinite scrolling */
+ INFINITE_SCROLL_DEBOUNCE_MS : 40,
+ /** @type {Number} number of px (or less) from the bottom the scrollbar should be before fetching */
+ INFINITE_SCROLL_FETCH_THRESHOLD_PX : 128,
+
+ /** override to track the scroll container for this view */
+ _setUpBehaviors : function( $where ){
+ var self = this,
+ $newRender = _super.prototype._setUpBehaviors.call( this, $where );
+ // this needs to be handled outside the events hash since we're accessing the scollContainer
+ // (rebind and debounce the method so we can cache for any later removal)
+ self.scrollHandler = _.debounce( _.bind( this.scrollHandler, self ), self.INFINITE_SCROLL_DEBOUNCE_MS );
+ self.$scrollContainer( $where ).on( 'scroll', self.scrollHandler );
+ return self;
+ },
+
+ /** */
+ scrollHandler : function( ev ){
+ var self = this;
+ var pxToBottom = self._scrollDistanceToBottom();
+
+ // if the scrollbar is past the trigger point, we're not already fetching,
+ // AND we're not displaying some panel OVER this one: fetch more contents
+ // note: is( :visible ) won't work here - it's still visible when this is covered with other panels
+ if( pxToBottom < self.INFINITE_SCROLL_FETCH_THRESHOLD_PX && !self._fetching && _.isEmpty( self.panelStack ) ){
+ self.listenToOnce( self.model.contents, 'sync', self.bulkAppendItemViews );
+ // TODO: gotta be a better way than a _fetching flag
+ self._fetching = true;
+ self.model.contents.fetchMore({ silent : true, useSync: true })
+ .always( function(){ delete self._fetching; });
+ }
+ },
+
+ /** return the number of px the scrollbar has until it bottoms out */
+ _scrollDistanceToBottom : function(){
+ var $container = this.$scrollContainer();
+ var pxToBottom = this.$el.outerHeight() - ( $container.scrollTop() + $container.innerHeight() );
+ return pxToBottom;
+ },
+
+ /** */
+ showContentsLoadingIndicator : function( speed ){
+ speed = _.isNumber( speed )? speed : this.fxSpeed;
+ if( this.$emptyMessage().is( ':visible' ) ){
+ this.$emptyMessage().hide();
+ }
+ // look for an existing indicator and stop all animations on it, otherwise make one
+ var $indicator = this.$list().find( '.contents-loading-indicator' );
+ if( $indicator.size() ){
+ $indicator.clearQueue();
+ $indicator.stop();
+ } else {
+ $indicator = $( '
' + _l( 'Loading...' ) + '
' ).hide();
+ }
+ // move it to the bottom and fade it in
+ return $indicator
+ .insertAfter( this.$( '> .list-items' ) )
+ .fadeIn( speed );
+ },
+
+ /** */
+ hideContentsLoadingIndicator : function( speed ){
+ speed = _.isNumber( speed )? speed : this.fxSpeed;
+ this.$( '> .contents-loading-indicator' ).slideUp({ duration: speed, complete: function _complete(){
+ $( this ).remove();
+ }});
+ },
+
+ // ------------------------------------------------------------------------ loading history/hda models
+ /** */
+ loadHistory : function( historyId, options, contentsOptions ){
+ this.info( 'loadHistory:', historyId, options, contentsOptions );
+ var self = this;
+ self.setModel( new HISTORY_MODEL.History({ id : historyId }) );
+ //TODO:?? cache histories?
+
+ self.trigger( 'loading' );
+ return self.model
+ .fetchWithContents( options, contentsOptions )
+ .always( function(){
+ self.trigger( 'loading-done' );
+ });
+ },
+
+ /** convenience alias to the model. Updates the item list only (not the history) */
+ refreshContents : function( options ){
+ if( this.model ){
+ return this.model.refresh( options );
+ }
+ // may have callbacks - so return an empty promise
+ return $.when();
+ },
+
+ /** release/free/shutdown old models and set up panel for new models
+ * @fires new-model with the panel as parameter
+ */
+ setModel : function( model, attributes ){
+ attributes = attributes || {};
+ _super.prototype.setModel.call( this, model, attributes );
+ if( this.model ){
+ this._setUpWebStorage();
+ }
+ },
+
+ /** Override to reset web storage when the id changes (since it needs the id) */
+ _setUpModelListeners : function(){
+ _super.prototype._setUpModelListeners.call( this );
+ return this.listenTo( this.model, {
+ 'change:id' : this._setUpWebStorage,
+ });
+ },
+
+ /** Override to reset web storage when the id changes (since it needs the id) */
+ _setUpCollectionListeners : function(){
+ _super.prototype._setUpCollectionListeners.call( this );
+ return this.listenTo( this.collection, {
+ // 'all' : function(){ console.log( this.collection + ':', arguments ); },
+ 'fetching-more' : this.showContentsLoadingIndicator,
+ 'fetching-more-done': this.hideContentsLoadingIndicator,
+ });
+ },
+
+ // ------------------------------------------------------------------------ browser stored prefs
+ /** Set up client side storage. Currently PersistanStorage keyed under 'history:'
+ * @see PersistentStorage
+ */
+ _setUpWebStorage : function(){
+ if( !this.model || !this.model.id ){ return this; }
+ //this.log( '_setUpWebStorage', initiallyExpanded, show_deleted, show_hidden );
+ if( this.storage ){
+ this.stopListening( this.storage );
+ }
+
+ this.storage = new HISTORY_PREFS.HistoryPrefs({
+ id: HISTORY_PREFS.HistoryPrefs.historyStorageKey( this.model.get( 'id' ) )
+ });
+ this.trigger( 'new-storage', this.storage, this );
+ this.log( this + ' (init\'d) storage:', this.storage.get() );
+
+ // TODO: reverse this - have storage reflect what's used in the view and not visversa
+ this.listenTo( this.storage, {
+ 'change:show_deleted' : function( view, newVal ){
+ this.showDeleted = newVal;
+ },
+ 'change:show_hidden' : function( view, newVal ){
+ this.showHidden = newVal;
+ }
+ }, this );
+ this.showDeleted = this.storage.get( 'show_deleted' ) || false;
+ this.showHidden = this.storage.get( 'show_hidden' ) || false;
+
+ return this;
+ },
+
+ // ------------------------------------------------------------------------ panel rendering
+ /** In this override, add a btn to toggle the selectors */
+ _buildNewRender : function(){
+ var $newRender = _super.prototype._buildNewRender.call( this );
+ this._renderSelectButton( $newRender );
+ return $newRender;
+ },
+
+ /** button for starting select mode */
+ _renderSelectButton : function( $where ){
+ $where = $where || this.$el;
+ // do not render selector option if no actions
+ if( !this.multiselectActions().length ){
+ return null;
+ }
+ // do not render (and remove even) if nothing to select
+ if( !this.views.length ){
+ this.hideSelectors();
+ $where.find( '.controls .actions .show-selectors-btn' ).remove();
+ return null;
+ }
+ // don't bother rendering if there's one already
+ var $existing = $where.find( '.controls .actions .show-selectors-btn' );
+ if( $existing.size() ){
+ return $existing;
+ }
+
+ return faIconButton({
+ title : _l( 'Operations on multiple datasets' ),
+ classes : 'show-selectors-btn',
+ faIcon : 'fa-check-square-o'
+ }).prependTo( $where.find( '.controls .actions' ) );
+ },
+
+ // ------------------------------------------------------------------------ sub-views
+ /** In this override, since history contents are mixed,
+ * get the appropo view class based on history_content_type
+ */
+ _getItemViewClass : function( model ){
+ var contentType = model.get( "history_content_type" );
+ switch( contentType ){
+ case 'dataset':
+ return this.HDAViewClass;
+ case 'dataset_collection':
+ return this.HDCAViewClass;
+ }
+ throw new TypeError( 'Unknown history_content_type: ' + contentType );
+ },
+
+ /** in this override, check if the contents would also display based on show_deleted/hidden */
+ _filterItem : function( model ){
+ var panel = this;
+ return ( _super.prototype._filterItem.call( panel, model )
+ && ( !model.hidden() || panel.showHidden )
+ && ( !model.isDeletedOrPurged() || panel.showDeleted ) );
+ },
+
+ /** in this override, add a linktarget, and expand if id is in web storage */
+ _getItemViewOptions : function( model ){
+ var options = _super.prototype._getItemViewOptions.call( this, model );
+ return _.extend( options, {
+ linkTarget : this.linkTarget,
+ expanded : !!this.storage.get( 'expandedIds' )[ model.id ],
+ hasUser : this.model.ownedByCurrUser()
+ });
+ },
+
+ /** In this override, add/remove expanded/collapsed model ids to/from web storage */
+ _setUpItemViewListeners : function( view ){
+ var panel = this;
+ _super.prototype._setUpItemViewListeners.call( panel, view );
+ //TODO:?? could use 'view:expanded' here?
+ // maintain a list of items whose bodies are expanded
+ return panel.listenTo( view, {
+ 'expanded': function( v ){
+ panel.storage.addExpanded( v.model );
+ },
+ 'collapsed': function( v ){
+ panel.storage.removeExpanded( v.model );
+ }
+ });
+ },
+
+ // ------------------------------------------------------------------------ selection
+ /** Override to correctly set the historyId of the new collection */
+ getSelectedModels : function(){
+ var collection = _super.prototype.getSelectedModels.call( this );
+ collection.historyId = this.collection.historyId;
+ return collection;
+ },
+
+ // ------------------------------------------------------------------------ panel events
+ /** event map */
+ events : _.extend( _.clone( _super.prototype.events ), {
+ // toggle list item selectors
+ 'click .show-selectors-btn' : 'toggleSelectors',
+ // allow (error) messages to be clicked away
+ 'click .messages [class$=message]' : 'clearMessages'
+ }),
+
+ /** Toggle and store the deleted visibility and re-render items
+ * @returns {Boolean} new show_deleted setting
+ */
+ toggleShowDeleted : function( show, store ){
+ show = ( show !== undefined )?( show ):( !this.showDeleted );
+ store = ( store !== undefined )?( store ):( true );
+ var self = this;
+
+ self.showDeleted = show;
+ //TODO: at this point deleted/hidden makes more sense (simpler) in the collection
+ self.model.contents.includeDeleted = show;
+ self.trigger( 'show-deleted', show );
+ if( store ){ self.storage.set( 'show_deleted', show ); }
+
+ // this seems brittle or at least not cohesive with the other non-hid approaches
+ var lastHid = self.collection.last().get( 'hid' );
+ console.log( 'lastHid:', lastHid );
+ var fetch = jQuery.when();
+ if( show ){
+ fetch = self.model.contents.fetchDeleted({
+ silent : true,
+ filters : { 'hid-ge' : lastHid }
+ });
+ }
+ fetch.done( function(){ self.renderItems(); });
+
+ return self.showDeleted;
+ },
+
+ /** Toggle and store whether to render explicity hidden contents
+ * @returns {Boolean} new show_hidden setting
+ */
+ toggleShowHidden : function( show, store ){
+ show = ( show !== undefined )?( show ):( !this.showHidden );
+ store = ( store !== undefined )?( store ):( true );
+ var self = this;
+
+ self.showHidden = show;
+ self.model.contents.includeHidden = show;
+ self.trigger( 'show-hidden', show );
+ if( store ){
+ self.storage.set( 'show_hidden', show );
+ }
+ var fetch = jQuery.when();
+ if( show ){
+ fetch = self.model.contents.fetchHidden({
+ silent : true,
+ });
+ }
+ fetch.done( function(){ self.renderItems(); });
+
+ return self.showHidden;
+ },
+
+ /** On the first search, if there are no details - load them, then search */
+ _firstSearch : function( searchFor ){
+ var self = this,
+ inputSelector = '> .controls .search-input';
+ this.log( 'onFirstSearch', searchFor );
+
+ console.log( '_firstSearch:', self.model.contents.haveDetails() );
+ if( self.model.contents.haveDetails() ){
+ self.searchItems( searchFor );
+ return;
+ }
+
+ self.$el.find( inputSelector ).searchInput( 'toggle-loading' );
+ // self.model.contents.fetchAllDetails({ silent: true })
+ self.model.contents.progressivelyFetchDetails({ silent: true })
+ .progress( function( response, limit, offset ){
+ // console.log( 'progress:', offset, offset + response.length );
+ // self.renderItems();
+ self.listenToOnce( self.model.contents, 'sync', self.bulkAppendItemViews );
+ // self.bulkAppendItemViews( self.model.contents, response, {});
+ })
+ .always( function(){
+ self.$el.find( inputSelector ).searchInput( 'toggle-loading' );
+ })
+ .done( function(){
+ self.searchItems( self.searchFor );
+ });
+ },
+
+ //TODO: break this out
+ // ........................................................................ error handling
+ /** Event handler for errors (from the panel, the history, or the history's contents)
+ * @param {Model or View} model the (Backbone) source of the error
+ * @param {XMLHTTPRequest} xhr any ajax obj. assoc. with the error
+ * @param {Object} options the options map commonly used with bbone ajax
+ * @param {String} msg optional message passed to ease error location
+ * @param {Object} msg optional object containing error details
+ */
+ errorHandler : function( model, xhr, options, msg, details ){
+ this.error( model, xhr, options, msg, details );
+
+ // interrupted ajax
+ if( xhr && xhr.status === 0 && xhr.readyState === 0 ){
+ //TODO: gmail style 'retrying in Ns'
+
+ // bad gateway
+ } else if( xhr && xhr.status === 502 ){
+ //TODO: gmail style 'retrying in Ns'
+
+ // otherwise, show an error message inside the panel
+ } else {
+ // if sentry is available, attempt to get the event id
+ var parsed = this._parseErrorMessage( model, xhr, options, msg, details );
+ // it's possible to have a triggered error before the message container is rendered - wait for it to show
+ if( !this.$messages().is( ':visible' ) ){
+ this.once( 'rendered', function(){
+ this.displayMessage( 'error', parsed.message, parsed.details );
+ });
+ } else {
+ this.displayMessage( 'error', parsed.message, parsed.details );
+ }
+ }
+ },
+
+ /** Parse an error event into an Object usable by displayMessage based on the parameters
+ * note: see errorHandler for more info on params
+ */
+ _parseErrorMessage : function( model, xhr, options, msg, details, sentryId ){
+ //if( xhr.responseText ){
+ // xhr.responseText = _.escape( xhr.responseText );
+ //}
+ var user = Galaxy.user,
+ // add the args (w/ some extra info) into an obj
+ parsed = {
+ message : this._bePolite( msg ),
+ details : {
+ message : msg,
+ raven : ( window.Raven && _.isFunction( Raven.lastEventId) )?
+ ( Raven.lastEventId() ):( undefined ),
+ agent : navigator.userAgent,
+ // add ajax data from Galaxy object cache
+ url : ( window.Galaxy )?( Galaxy.lastAjax.url ):( undefined ),
+ data : ( window.Galaxy )?( Galaxy.lastAjax.data ):( undefined ),
+ options : ( xhr )?( _.omit( options, 'xhr' ) ):( options ),
+ xhr : xhr,
+ source : ( _.isFunction( model.toJSON ) )?( model.toJSON() ):( model + '' ),
+ user : ( user instanceof USER.User )?( user.toJSON() ):( user + '' )
+ }
+ };
+
+ // add any extra details passed in
+ _.extend( parsed.details, details || {} );
+ // fancy xhr.header parsing (--> obj)
+ if( xhr && _.isFunction( xhr.getAllResponseHeaders ) ){
+ var responseHeaders = xhr.getAllResponseHeaders();
+ responseHeaders = _.compact( responseHeaders.split( '\n' ) );
+ responseHeaders = _.map( responseHeaders, function( header ){
+ return header.split( ': ' );
+ });
+ parsed.details.xhr.responseHeaders = _.object( responseHeaders );
+ }
+ return parsed;
+ },
+
+ /** Modify an error message to be fancy and wear a monocle. */
+ _bePolite : function( msg ){
+ msg = msg || _l( 'An error occurred while getting updates from the server' );
+ return msg + '. ' + _l( 'Please contact a Galaxy administrator if the problem persists' ) + '.';
+ },
+
+ // ........................................................................ (error) messages
+ /** Display a message in the top of the panel.
+ * @param {String} type type of message ('done', 'error', 'warning')
+ * @param {String} msg the message to display
+ * @param {Object or HTML} modal contents displayed when the user clicks 'details' in the message
+ */
+ displayMessage : function( type, msg, details ){
+ //precondition: msgContainer must have been rendered even if there's no model
+ var panel = this;
+ //this.log( 'displayMessage', type, msg, details );
+
+ this.scrollToTop();
+ var $msgContainer = this.$messages(),
+ $msg = $( '' ).addClass( type + 'message' ).html( msg );
+ //this.log( ' ', $msgContainer );
+
+ if( !_.isEmpty( details ) ){
+ var $detailsLink = $( 'Details' )
+ .click( function(){
+ Galaxy.modal.show( panel._messageToModalOptions( type, msg, details ) );
+ return false;
+ });
+ $msg.append( ' ', $detailsLink );
+ }
+ return $msgContainer.append( $msg );
+ },
+
+ /** convert msg and details into modal options usable by Galaxy.modal */
+ _messageToModalOptions : function( type, msg, details ){
+ // only error is fleshed out here
+ var panel = this,
+ options = { title: 'Details' };
+ if( _.isObject( details ) ){
+
+ details = _.omit( details, _.functions( details ) );
+ var text = JSON.stringify( details, null, ' ' ),
+ pre = $( '' ).text( text );
+ options.body = $( '' ).append( pre );
+
+ } else {
+ options.body = $( '' ).html( details );
+ }
+
+ options.buttons = {
+ 'Ok': function(){
+ Galaxy.modal.hide();
+ panel.clearMessages();
+ }
+ //TODO: if( type === 'error' ){ options.buttons[ 'Report this error' ] = function(){} }
+ };
+ return options;
+ },
+
+ /** Remove all messages from the panel. */
+ clearMessages : function( ev ){
+ var $target = !_.isUndefined( ev )?
+ $( ev.currentTarget )
+ :this.$messages().children( '[class$="message"]' );
+ $target.fadeOut( this.fxSpeed, function(){
+ $( this ).remove();
+ });
+ return this;
+ },
+
+ // ........................................................................ scrolling
+ /** Scrolls the panel to show the content sub-view with the given hid.
+ * @param {Integer} hid the hid of item to scroll into view
+ * @returns {HistoryView} the panel
+ */
+ scrollToHid : function( hid ){
+ return this.scrollToItem( _.first( this.viewsWhereModel({ hid: hid }) ) );
+ },
+
+ // ........................................................................ misc
+ /** Return a string rep of the history */
+ toString : function(){
+ return 'HistoryView(' + (( this.model )?( this.model.get( 'name' )):( '' )) + ')';
+ }
+ });
+
+
+ //------------------------------------------------------------------------------ TEMPLATES
+ HistoryView.prototype.templates = (function(){
+
+ var controlsTemplate = BASE_MVC.wrapTemplate([
+ '
',
+ // space for title bar buttons - gen. floated to the right
+ '',
+ '',
+
+ // expandable area for more details
+ '',
+ '
'
+ ]);
+
+ var warnings = {};
+
+ var titleBarTemplate = BASE_MVC.wrapTemplate([
+ // adding a tabindex here allows focusing the title bar and the use of keydown to expand the dataset display
+ '
'
+ ], 'dataset' );
+
+ // messages to be displayed only within the details section ('below the fold')
+ var detailMessageTemplates = {
+ resubmitted : BASE_MVC.wrapTemplate([
+ // deleted not purged
+ '<% if( model.resubmitted ){ %>',
+ '
',
+ _l( 'The job creating this dataset has been resubmitted' ),
+ '
',
+ '<% } %>'
+ ])
+ };
+
+ // this is applied to both old and new style display apps
+ var displayApplicationsTemplate = BASE_MVC.wrapTemplate([
+ '<% _.each( apps, function( app ){ %>',
+ '
',
+ '<% }); %>'
+ ], 'apps' );
+
+ return _.extend( {}, _super.prototype.templates, {
+ warnings : warnings,
+ details : detailsTemplate,
+ noAccess : noAccessTemplate,
+ summaries : summaryTemplates,
+ detailMessages : detailMessageTemplates,
+ displayApplications : displayApplicationsTemplate
+ });
+ }());
+
+
+ // ============================================================================
+ return {
+ DatasetListItemView : DatasetListItemView
+ };
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! jquery */ 3), __webpack_require__(/*! libs/backbone */ 2), __webpack_require__(/*! jquery */ 3), __webpack_require__(/*! underscore */ 1)))
+
+/***/ },
+/* 86 */
+/*!*********************************************!*\
+ !*** ./galaxy/scripts/ui/fa-icon-button.js ***!
+ \*********************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function($, _) {(function (root, factory) {
+ if (true) {
+ !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+ } else {
+ root.faIconButton = factory();
+ }
+
+ }(this, function () {
+ //============================================================================
+ /** Returns a jQuery object containing a clickable font-awesome button.
+ * options:
+ * tooltipConfig : option map for bootstrap tool tip
+ * classes : array of class names (will always be classed as icon-btn)
+ * disabled : T/F - add the 'disabled' class?
+ * title : tooltip/title string
+ * target : optional href target
+ * href : optional href
+ * faIcon : which font awesome icon to use
+ * onclick : function to call when the button is clicked
+ */
+ var faIconButton = function( options ){
+ options = options || {};
+ options.tooltipConfig = options.tooltipConfig || { placement: 'bottom' };
+
+ options.classes = [ 'icon-btn' ].concat( options.classes || [] );
+ if( options.disabled ){
+ options.classes.push( 'disabled' );
+ }
+
+ var html = [
+ '',
+ // could go with something less specific here - like 'html'
+ '',
+ ''
+ ].join( '' );
+ var $button = $( html ).tooltip( options.tooltipConfig );
+ if( _.isFunction( options.onclick ) ){
+ $button.click( options.onclick );
+ }
+ return $button;
+ };
+
+ //============================================================================
+ return faIconButton;
+ }));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! jquery */ 3), __webpack_require__(/*! underscore */ 1)))
+
+/***/ },
+/* 87 */
+/*!***********************************************!*\
+ !*** ./galaxy/scripts/mvc/history/hdca-li.js ***!
+ \***********************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(_) {!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
+ __webpack_require__(/*! mvc/dataset/states */ 70),
+ __webpack_require__(/*! mvc/collection/collection-li */ 88),
+ __webpack_require__(/*! mvc/collection/collection-view */ 89),
+ __webpack_require__(/*! mvc/base-mvc */ 5),
+ __webpack_require__(/*! utils/localization */ 7)
+ ], __WEBPACK_AMD_DEFINE_RESULT__ = function( STATES, DC_LI, DC_VIEW, BASE_MVC, _l ){
+
+ 'use strict';
+
+ //==============================================================================
+ var _super = DC_LI.DCListItemView;
+ /** @class Read only view for HistoryDatasetCollectionAssociation (a dataset collection inside a history).
+ */
+ var HDCAListItemView = _super.extend(
+ /** @lends HDCAListItemView.prototype */{
+
+ /** logger used to record this.log messages, commonly set to console */
+ //logger : console,
+
+ className : _super.prototype.className + " history-content",
+
+ /** event listeners */
+ _setUpListeners : function(){
+ _super.prototype._setUpListeners.call( this );
+
+ this.listenTo( this.model, {
+ 'change:populated change:visible' : function( model, options ){ this.render(); },
+ });
+ },
+
+ /** Override to provide the proper collections panels as the foldout */
+ _getFoldoutPanelClass : function(){
+ switch( this.model.get( 'collection_type' ) ){
+ case 'list':
+ return DC_VIEW.ListCollectionView;
+ case 'paired':
+ return DC_VIEW.PairCollectionView;
+ case 'list:paired':
+ return DC_VIEW.ListOfPairsCollectionView;
+ }
+ throw new TypeError( 'Uknown collection_type: ' + this.model.get( 'collection_type' ) );
+ },
+
+ /** In this override, add the state as a class for use with state-based CSS */
+ _swapNewRender : function( $newRender ){
+ _super.prototype._swapNewRender.call( this, $newRender );
+ //TODO: model currently has no state
+ var state = !this.model.get( 'populated' ) ? STATES.RUNNING : STATES.OK;
+ //if( this.model.has( 'state' ) ){
+ this.$el.addClass( 'state-' + state );
+ //}
+ return this.$el;
+ },
+
+ // ......................................................................... misc
+ /** String representation */
+ toString : function(){
+ var modelString = ( this.model )?( this.model + '' ):( '(no model)' );
+ return 'HDCAListItemView(' + modelString + ')';
+ }
+ });
+
+ /** underscore templates */
+ HDCAListItemView.prototype.templates = (function(){
+
+ var warnings = _.extend( {}, _super.prototype.templates.warnings, {
+ hidden : BASE_MVC.wrapTemplate([
+ // add a warning when hidden
+ '<% if( !collection.visible ){ %>',
+ '
',
+ _l( 'This collection has been hidden' ),
+ '
',
+ '<% } %>'
+ ], 'collection' )
+ });
+
+ // could steal this from hda-base (or use mixed content)
+ var titleBarTemplate = BASE_MVC.wrapTemplate([
+ // adding the hid display to the title
+ '
',
+ '',
+ '
',
+ //TODO: remove whitespace and use margin-right
+ '<%- collection.hid %> ',
+ '<%- collection.name %>',
+ '
',
+ '',
+ '
'
+ ], 'collection' );
+
+ return _.extend( {}, _super.prototype.templates, {
+ warnings : warnings,
+ titleBar : titleBarTemplate
+ });
+ }());
+
+
+ //==============================================================================
+ return {
+ HDCAListItemView : HDCAListItemView
+ };
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! underscore */ 1)))
+
+/***/ },
+/* 88 */
+/*!********************************************************!*\
+ !*** ./galaxy/scripts/mvc/collection/collection-li.js ***!
+ \********************************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(_, $, jQuery) {!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
+ __webpack_require__(/*! mvc/list/list-item */ 81),
+ __webpack_require__(/*! mvc/dataset/dataset-li */ 85),
+ __webpack_require__(/*! mvc/base-mvc */ 5),
+ __webpack_require__(/*! utils/localization */ 7)
+ ], __WEBPACK_AMD_DEFINE_RESULT__ = function( LIST_ITEM, DATASET_LI, BASE_MVC, _l ){
+
+ 'use strict';
+ //==============================================================================
+ var FoldoutListItemView = LIST_ITEM.FoldoutListItemView,
+ ListItemView = LIST_ITEM.ListItemView;
+ /** @class Read only view for DatasetCollection.
+ */
+ var DCListItemView = FoldoutListItemView.extend(
+ /** @lends DCListItemView.prototype */{
+ //TODO: may not be needed
+
+ /** logger used to record this.log messages, commonly set to console */
+ //logger : console,
+
+ className : FoldoutListItemView.prototype.className + " dataset-collection",
+ id : function(){
+ return [ 'dataset_collection', this.model.get( 'id' ) ].join( '-' );
+ },
+
+ /** override to add linkTarget */
+ initialize : function( attributes ){
+ this.linkTarget = attributes.linkTarget || '_blank';
+ this.hasUser = attributes.hasUser;
+ FoldoutListItemView.prototype.initialize.call( this, attributes );
+ },
+
+ /** event listeners */
+ _setUpListeners : function(){
+ FoldoutListItemView.prototype._setUpListeners.call( this );
+ // re-rendering on deletion
+ this.listenTo( this.model, 'change', function( model, options ){
+ if( _.isEqual( _.keys( model.changed ), [ 'deleted' ] ) ){
+ this.render();
+ }
+ });
+ },
+
+ // ......................................................................... rendering
+ //TODO:?? possibly move to listItem
+ /** render a subtitle to show the user what sort of collection this is */
+ _renderSubtitle : function(){
+ var $subtitle = $( '' );
+ //TODO: would be good to get this in the subtitle
+ //var len = this.model.elements.length;
+ switch( this.model.get( 'collection_type' ) ){
+ case 'list':
+ return $subtitle.text( _l( 'a list of datasets' ) );
+ case 'paired':
+ return $subtitle.text( _l( 'a pair of datasets' ) );
+ case 'list:paired':
+ return $subtitle.text( _l( 'a list of paired datasets' ) );
+ }
+ return $subtitle;
+ },
+
+ // ......................................................................... foldout
+ /** override to add linktarget to sub-panel */
+ _getFoldoutPanelOptions : function(){
+ var options = FoldoutListItemView.prototype._getFoldoutPanelOptions.call( this );
+ return _.extend( options, {
+ linkTarget : this.linkTarget,
+ hasUser : this.hasUser
+ });
+ },
+
+ /** override to not catch sub-panel selectors */
+ $selector : function(){
+ return this.$( '> .selector' );
+ },
+
+ // ......................................................................... misc
+ /** String representation */
+ toString : function(){
+ var modelString = ( this.model )?( this.model + '' ):( '(no model)' );
+ return 'DCListItemView(' + modelString + ')';
+ }
+ });
+
+ // ............................................................................ TEMPLATES
+ /** underscore templates */
+ DCListItemView.prototype.templates = (function(){
+
+ var warnings = _.extend( {}, FoldoutListItemView.prototype.templates.warnings, {
+ error : BASE_MVC.wrapTemplate([
+ // error during index fetch - show error on dataset
+ '<% if( model.error ){ %>',
+ '
',
+ _l( 'There was an error getting the data for this collection' ), ': <%- model.error %>',
+ '
', _l([
+ 'Pair collections are permanent collections containing two datasets: one forward and one reverse. ',
+ 'Often these are forward and reverse reads. The pair collections can be passed to tools and ',
+ 'workflows in order to have analyses done on both datasets. This interface allows ',
+ 'you to create a pair, name it, and swap which is forward and which reverse.'
+ ].join( '' )), '
',
+ '
',
+ '
', _l([
+ 'Click the "Swap" link to make your forward dataset the reverse ',
+ 'and the reverse dataset forward.'
+ ].join( '' )), '
',
+ '
', _l([
+ 'Click the "Cancel" button to exit the interface.'
+ ].join( '' )), '
',
+ '
',
+ '
', _l([
+ 'Once your collection is complete, enter a name and ',
+ 'click "Create list".'
+ ].join( '' )), '
'
+ ].join('')),
+
+ /** a simplified page communicating what went wrong and why the user needs to reselect something else */
+ invalidInitial : _.template([
+ '
',
+ '
',
+ '',
+ '<% if( _.size( problems ) ){ %>',
+ _l( 'The following selections could not be included due to problems' ),
+ '
<% _.each( problems, function( problem ){ %>',
+ '
<%- problem.element.name %>: <%- problem.text %>
',
+ '<% }); %>
',
+ '<% } else if( _.size( elements ) === 0 ){ %>',
+ _l( 'No datasets were selected' ), '.',
+ '<% } else if( _.size( elements ) === 1 ){ %>',
+ _l( 'Only one dataset was selected' ), ': <%- elements[0].name %>',
+ '<% } else if( _.size( elements ) > 2 ){ %>',
+ _l( 'Too many datasets were selected' ),
+ ': <%- _.pluck( elements, "name" ).join( ", ") %>',
+ '<% } %>',
+ ' ',
+ _l( 'Two (and only two) elements are needed for the pair' ), '. ',
+ _l( 'You may need to ' ),
+ '', _l( 'cancel' ), ' ',
+ _l( 'and reselect new elements' ), '.',
+ '',
+ '
',
+ '
',
+ ''
+ ].join('')),
+ }),
+
+ // ------------------------------------------------------------------------ misc
+ /** string rep */
+ toString : function(){ return 'PairCollectionCreator'; }
+ });
+
+
+ //==============================================================================
+ /** List collection flavor of collectionCreatorModal. */
+ var pairCollectionCreatorModal = function _pairCollectionCreatorModal( elements, options ){
+ options = options || {};
+ options.title = _l( 'Create a collection from a pair of datasets' );
+ return LIST_CREATOR.collectionCreatorModal( elements, options, PairCollectionCreator );
+ };
+
+
+ //==============================================================================
+ /** Use a modal to create a pair collection, then add it to the given history contents.
+ * @returns {Deferred} resolved when the collection is added to the history.
+ */
+ function createPairCollection( contents ){
+ var elements = contents.toJSON(),
+ promise = pairCollectionCreatorModal( elements, {
+ creationFn : function( elements, name ){
+ elements = [
+ { name: "forward", src: "hda", id: elements[0].id },
+ { name: "reverse", src: "hda", id: elements[1].id }
+ ];
+ return contents.createHDCA( elements, 'paired', name );
+ }
+ });
+ return promise;
+ }
+
+ //==============================================================================
+ return {
+ PairCollectionCreator : PairCollectionCreator,
+ pairCollectionCreatorModal : pairCollectionCreatorModal,
+ createPairCollection : createPairCollection,
+ };
+ }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+ /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(/*! libs/backbone */ 2), __webpack_require__(/*! underscore */ 1), __webpack_require__(/*! jquery */ 3)))
+
+/***/ },
+/* 99 */
+/*!***************************************************************************!*\
+ !*** ./galaxy/scripts/mvc/collection/list-of-pairs-collection-creator.js ***!
+ \***************************************************************************/
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(Backbone, _, jQuery, $) {!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
+ __webpack_require__(/*! utils/levenshtein */ 100),
+ __webpack_require__(/*! utils/natural-sort */ 77),
+ __webpack_require__(/*! mvc/collection/list-collection-creator */ 76),
+ __webpack_require__(/*! mvc/base-mvc */ 5),
+ __webpack_require__(/*! utils/localization */ 7),
+ __webpack_require__(/*! ui/hoverhighlight */ 78)
+ ], __WEBPACK_AMD_DEFINE_RESULT__ = function( levenshteinDistance, naturalSort, LIST_COLLECTION_CREATOR, baseMVC, _l ){
+
+ 'use strict';
+
+ var logNamespace = 'collections';
+ /* ============================================================================
+ TODO:
+
+
+ PROGRAMMATICALLY:
+ currPanel.once( 'rendered', function(){
+ currPanel.showSelectors();
+ currPanel.selectAll();
+ _.last( currPanel.actionsPopup.options ).func();
+ });
+
+ ============================================================================ */
+ /** A view for paired datasets in the collections creator.
+ */
+ var PairView = Backbone.View.extend( baseMVC.LoggableMixin ).extend({
+ _logNamespace : logNamespace,
+
+ tagName : 'li',
+ className : 'dataset paired',
+
+ initialize : function( attributes ){
+ this.pair = attributes.pair || {};
+ },
+
+ template : _.template([
+ '<%- pair.forward.name %>',
+ '',
+ '<%- pair.name %>',
+ '',
+ '<%- pair.reverse.name %>'
+ ].join('')),
+
+ render : function(){
+ this.$el
+ .attr( 'draggable', true )
+ .data( 'pair', this.pair )
+ .html( this.template({ pair: this.pair }) )
+ .addClass( 'flex-column-container' );
+ return this;
+ },
+
+ events : {
+ 'dragstart' : '_dragstart',
+ 'dragend' : '_dragend',
+ 'dragover' : '_sendToParent',
+ 'drop' : '_sendToParent'
+ },
+
+ /** dragging pairs for re-ordering */
+ _dragstart : function( ev ){
+ ev.currentTarget.style.opacity = '0.4';
+ if( ev.originalEvent ){ ev = ev.originalEvent; }
+
+ ev.dataTransfer.effectAllowed = 'move';
+ ev.dataTransfer.setData( 'text/plain', JSON.stringify( this.pair ) );
+
+ this.$el.parent().trigger( 'pair.dragstart', [ this ] );
+ },
+
+ /** dragging pairs for re-ordering */
+ _dragend : function( ev ){
+ ev.currentTarget.style.opacity = '1.0';
+ this.$el.parent().trigger( 'pair.dragend', [ this ] );
+ },
+
+ /** manually bubble up an event to the parent/container */
+ _sendToParent : function( ev ){
+ this.$el.parent().trigger( ev );
+ },
+
+ /** string rep */
+ toString : function(){
+ return 'PairView(' + this.pair.name + ')';
+ }
+ });
+
+
+ // ============================================================================
+ /** returns an autopair function that uses the provided options.match function */
+ function autoPairFnBuilder( options ){
+ options = options || {};
+ options.createPair = options.createPair || function _defaultCreatePair( params ){
+ params = params || {};
+ var a = params.listA.splice( params.indexA, 1 )[0],
+ b = params.listB.splice( params.indexB, 1 )[0],
+ aInBIndex = params.listB.indexOf( a ),
+ bInAIndex = params.listA.indexOf( b );
+ if( aInBIndex !== -1 ){ params.listB.splice( aInBIndex, 1 ); }
+ if( bInAIndex !== -1 ){ params.listA.splice( bInAIndex, 1 ); }
+ return this._pair( a, b, { silent: true });
+ };
+ // compile these here outside of the loop
+ var _regexps = [];
+ function getRegExps(){
+ if( !_regexps.length ){
+ _regexps = [
+ new RegExp( this.filters[0] ),
+ new RegExp( this.filters[1] )
+ ];
+ }
+ return _regexps;
+ }
+ // mangle params as needed
+ options.preprocessMatch = options.preprocessMatch || function _defaultPreprocessMatch( params ){
+ var regexps = getRegExps.call( this );
+ return _.extend( params, {
+ matchTo : params.matchTo.name.replace( regexps[0], '' ),
+ possible : params.possible.name.replace( regexps[1], '' )
+ });
+ };
+
+ return function _strategy( params ){
+ this.debug( 'autopair _strategy ---------------------------' );
+ params = params || {};
+ var listA = params.listA,
+ listB = params.listB,
+ indexA = 0, indexB,
+ bestMatch = {
+ score : 0.0,
+ index : null
+ },
+ paired = [];
+ //console.debug( 'params:', JSON.stringify( params, null, ' ' ) );
+ this.debug( 'starting list lens:', listA.length, listB.length );
+ this.debug( 'bestMatch (starting):', JSON.stringify( bestMatch, null, ' ' ) );
+
+ while( indexA < listA.length ){
+ var matchTo = listA[ indexA ];
+ bestMatch.score = 0.0;
+
+ for( indexB=0; indexB= scoreThreshold ){
+ //console.debug( 'autoPairFnBuilder.strategy', listA[ indexA ].name, listB[ bestMatch.index ].name );
+ paired.push( options.createPair.call( this, {
+ listA : listA,
+ indexA : indexA,
+ listB : listB,
+ indexB : bestMatch.index
+ }));
+ //console.debug( 'list lens now:', listA.length, listB.length );
+ } else {
+ indexA += 1;
+ }
+ if( !listA.length || !listB.length ){
+ return paired;
+ }
+ }
+ this.debug( 'paired:', JSON.stringify( paired, null, ' ' ) );
+ this.debug( 'autopair _strategy ---------------------------' );
+ return paired;
+ };
+ }
+
+
+ // ============================================================================
+ /** An interface for building collections of paired datasets.
+ */
+ var PairedCollectionCreator = Backbone.View.extend( baseMVC.LoggableMixin ).extend({
+ _logNamespace : logNamespace,
+
+ className: 'list-of-pairs-collection-creator collection-creator flex-row-container',
+
+ /** set up initial options, instance vars, behaviors, and autopair (if set to do so) */
+ initialize : function( attributes ){
+ this.metric( 'PairedCollectionCreator.initialize', attributes );
+ //this.debug( '-- PairedCollectionCreator:', attributes );
+
+ attributes = _.defaults( attributes, {
+ datasets : [],
+ filters : this.DEFAULT_FILTERS,
+ automaticallyPair : true,
+ strategy : 'lcs',
+ matchPercentage : 0.9,
+ twoPassAutopairing : true
+ });
+
+ /** unordered, original list */
+ this.initialList = attributes.datasets;
+
+ /** is this from a history? if so, what's its id? */
+ this.historyId = attributes.historyId;
+
+ /** which filters should be used initially? (String[2] or name in commonFilters) */
+ this.filters = this.commonFilters[ attributes.filters ] || this.commonFilters[ this.DEFAULT_FILTERS ];
+ if( _.isArray( attributes.filters ) ){
+ this.filters = attributes.filters;
+ }
+
+ /** try to auto pair the unpaired datasets on load? */
+ this.automaticallyPair = attributes.automaticallyPair;
+
+ /** what method to use for auto pairing (will be passed aggression level) */
+ this.strategy = this.strategies[ attributes.strategy ] || this.strategies[ this.DEFAULT_STRATEGY ];
+ if( _.isFunction( attributes.strategy ) ){
+ this.strategy = attributes.strategy;
+ }
+
+ /** distance/mismatch level allowed for autopairing */
+ this.matchPercentage = attributes.matchPercentage;
+
+ /** try to autopair using simple first, then this.strategy on the remainder */
+ this.twoPassAutopairing = attributes.twoPassAutopairing;
+
+ /** remove file extensions (\.*) from created pair names? */
+ this.removeExtensions = true;
+ //this.removeExtensions = false;
+
+ /** fn to call when the cancel button is clicked (scoped to this) - if falsy, no btn is displayed */
+ this.oncancel = attributes.oncancel;
+ /** fn to call when the collection is created (scoped to this) */
+ this.oncreate = attributes.oncreate;
+
+ /** fn to call when the cancel button is clicked (scoped to this) - if falsy, no btn is displayed */
+ this.autoscrollDist = attributes.autoscrollDist || 24;
+
+ /** is the unpaired panel shown? */
+ this.unpairedPanelHidden = false;
+ /** is the paired panel shown? */
+ this.pairedPanelHidden = false;
+
+ /** DOM elements currently being dragged */
+ this.$dragging = null;
+
+ /** Used for blocking UI events during ajax/operations (don't post twice) */
+ this.blocking = false;
+
+ this._setUpBehaviors();
+ this._dataSetUp();
+ },
+
+ /** map of common filter pairs by name */
+ commonFilters : {
+ illumina : [ '_1', '_2' ],
+ Rs : [ '_R1', '_R2' ]
+ },
+ /** which commonFilter to use by default */
+ DEFAULT_FILTERS : 'illumina',
+
+ /** map of name->fn for autopairing */
+ strategies : {
+ 'simple' : 'autopairSimple',
+ 'lcs' : 'autopairLCS',
+ 'levenshtein' : 'autopairLevenshtein'
+ },
+ /** default autopair strategy name */
+ DEFAULT_STRATEGY : 'lcs',
+
+ // ------------------------------------------------------------------------ process raw list
+ /** set up main data: cache initialList, sort, and autopair */
+ _dataSetUp : function(){
+ //this.debug( '-- _dataSetUp' );
+
+ this.paired = [];
+ this.unpaired = [];
+
+ this.selectedIds = [];
+
+ // sort initial list, add ids if needed, and save new working copy to unpaired
+ this._sortInitialList();
+ this._ensureIds();
+ this.unpaired = this.initialList.slice( 0 );
+
+ if( this.automaticallyPair ){
+ this.autoPair();
+ this.once( 'rendered:initial', function(){
+ this.trigger( 'autopair' );
+ });
+ }
+ },
+
+ /** sort initial list */
+ _sortInitialList : function(){
+ //this.debug( '-- _sortInitialList' );
+ this._sortDatasetList( this.initialList );
+ },
+
+ /** sort a list of datasets */
+ _sortDatasetList : function( list ){
+ // currently only natural sort by name
+ list.sort( function( a, b ){ return naturalSort( a.name, b.name ); });
+ return list;
+ },
+
+ /** add ids to dataset objs in initial list if none */
+ _ensureIds : function(){
+ this.initialList.forEach( function( dataset ){
+ if( !dataset.hasOwnProperty( 'id' ) ){
+ dataset.id = _.uniqueId();
+ }
+ });
+ return this.initialList;
+ },
+
+ /** split initial list into two lists, those that pass forward filters & those passing reverse */
+ _splitByFilters : function(){
+ var regexFilters = this.filters.map( function( stringFilter ){
+ return new RegExp( stringFilter );
+ }),
+ split = [ [], [] ];
+
+ function _filter( unpaired, filter ){
+ return filter.test( unpaired.name );
+ //return dataset.name.indexOf( filter ) >= 0;
+ }
+ this.unpaired.forEach( function _filterEach( unpaired ){
+ // 90% of the time this seems to work, but:
+ //TODO: this treats *all* strings as regex which may confuse people - possibly check for // surrounding?
+ // would need explanation in help as well
+ regexFilters.forEach( function( filter, i ){
+ if( _filter( unpaired, filter ) ){
+ split[i].push( unpaired );
+ }
+ });
+ });
+ return split;
+ },
+
+ /** add a dataset to the unpaired list in it's proper order */
+ _addToUnpaired : function( dataset ){
+ // currently, unpaired is natural sorted by name, use binary search to find insertion point
+ var binSearchSortedIndex = function( low, hi ){
+ if( low === hi ){ return low; }
+
+ var mid = Math.floor( ( hi - low ) / 2 ) + low,
+ compared = naturalSort( dataset.name, this.unpaired[ mid ].name );
+
+ if( compared < 0 ){
+ return binSearchSortedIndex( low, mid );
+ } else if( compared > 0 ){
+ return binSearchSortedIndex( mid + 1, hi );
+ }
+ // walk the equal to find the last
+ while( this.unpaired[ mid ] && this.unpaired[ mid ].name === dataset.name ){ mid++; }
+ return mid;
+
+ }.bind( this );
+
+ this.unpaired.splice( binSearchSortedIndex( 0, this.unpaired.length ), 0, dataset );
+ },
+
+ // ------------------------------------------------------------------------ auto pairing
+ /** two passes to automatically create pairs:
+ * use both simpleAutoPair, then the fn mentioned in strategy
+ */
+ autoPair : function( strategy ){
+ // split first using exact matching
+ var split = this._splitByFilters(),
+ paired = [];
+ if( this.twoPassAutopairing ){
+ paired = this.autopairSimple({
+ listA : split[0],
+ listB : split[1]
+ });
+ split = this._splitByFilters();
+ }
+
+ // uncomment to see printlns while running tests
+ //this.debug = function(){ console.log.apply( console, arguments ); };
+
+ // then try the remainder with something less strict
+ strategy = strategy || this.strategy;
+ split = this._splitByFilters();
+ paired = paired.concat( this[ strategy ].call( this, {
+ listA : split[0],
+ listB : split[1]
+ }));
+ return paired;
+ },
+
+ /** autopair by exact match */
+ autopairSimple : autoPairFnBuilder({
+ scoreThreshold: function(){ return 1.0; },
+ match : function _match( params ){
+ params = params || {};
+ if( params.matchTo === params.possible ){
+ return {
+ index: params.index,
+ score: 1.0
+ };
+ }
+ return params.bestMatch;
+ }
+ }),
+
+ /** autopair by levenshtein edit distance scoring */
+ autopairLevenshtein : autoPairFnBuilder({
+ scoreThreshold: function(){ return this.matchPercentage; },
+ match : function _matches( params ){
+ params = params || {};
+ var distance = levenshteinDistance( params.matchTo, params.possible ),
+ score = 1.0 - ( distance / ( Math.max( params.matchTo.length, params.possible.length ) ) );
+ if( score > params.bestMatch.score ){
+ return {
+ index: params.index,
+ score: score
+ };
+ }
+ return params.bestMatch;
+ }
+ }),
+
+ /** autopair by longest common substrings scoring */
+ autopairLCS : autoPairFnBuilder({
+ scoreThreshold: function(){ return this.matchPercentage; },
+ match : function _matches( params ){
+ params = params || {};
+ var match = this._naiveStartingAndEndingLCS( params.matchTo, params.possible ).length,
+ score = match / ( Math.max( params.matchTo.length, params.possible.length ) );
+ if( score > params.bestMatch.score ){
+ return {
+ index: params.index,
+ score: score
+ };
+ }
+ return params.bestMatch;
+ }
+ }),
+
+ /** return the concat'd longest common prefix and suffix from two strings */
+ _naiveStartingAndEndingLCS : function( s1, s2 ){
+ var fwdLCS = '',
+ revLCS = '',
+ i = 0, j = 0;
+ while( i < s1.length && i < s2.length ){
+ if( s1[ i ] !== s2[ i ] ){
+ break;
+ }
+ fwdLCS += s1[ i ];
+ i += 1;
+ }
+ if( i === s1.length ){ return s1; }
+ if( i === s2.length ){ return s2; }
+
+ i = ( s1.length - 1 );
+ j = ( s2.length - 1 );
+ while( i >= 0 && j >= 0 ){
+ if( s1[ i ] !== s2[ j ] ){
+ break;
+ }
+ revLCS = [ s1[ i ], revLCS ].join( '' );
+ i -= 1;
+ j -= 1;
+ }
+ return fwdLCS + revLCS;
+ },
+
+ // ------------------------------------------------------------------------ pairing / unpairing
+ /** create a pair from fwd and rev, removing them from unpaired, and placing the new pair in paired */
+ _pair : function( fwd, rev, options ){
+ options = options || {};
+ this.debug( '_pair:', fwd, rev );
+ var pair = this._createPair( fwd, rev, options.name );
+ this.paired.push( pair );
+ this.unpaired = _.without( this.unpaired, fwd, rev );
+ if( !options.silent ){
+ this.trigger( 'pair:new', pair );
+ }
+ return pair;
+ },
+
+ /** create a pair Object from fwd and rev, adding the name attribute (will guess if not given) */
+ _createPair : function( fwd, rev, name ){
+ // ensure existance and don't pair something with itself
+ if( !( fwd && rev ) || ( fwd === rev ) ){
+ throw new Error( 'Bad pairing: ' + [ JSON.stringify( fwd ), JSON.stringify( rev ) ] );
+ }
+ name = name || this._guessNameForPair( fwd, rev );
+ return { forward : fwd, name : name, reverse : rev };
+ },
+
+ /** try to find a good pair name for the given fwd and rev datasets */
+ _guessNameForPair : function( fwd, rev, removeExtensions ){
+ removeExtensions = ( removeExtensions !== undefined )?( removeExtensions ):( this.removeExtensions );
+ var fwdName = fwd.name,
+ revName = rev.name,
+ lcs = this._naiveStartingAndEndingLCS(
+ fwdName.replace( new RegExp( this.filters[0] ), '' ),
+ revName.replace( new RegExp( this.filters[1] ), '' )
+ );
+ if( removeExtensions ){
+ var lastDotIndex = lcs.lastIndexOf( '.' );
+ if( lastDotIndex > 0 ){
+ var extension = lcs.slice( lastDotIndex, lcs.length );
+ lcs = lcs.replace( extension, '' );
+ fwdName = fwdName.replace( extension, '' );
+ revName = revName.replace( extension, '' );
+ }
+ }
+ return lcs || ( fwdName + ' & ' + revName );
+ },
+
+ /** unpair a pair, removing it from paired, and adding the fwd,rev datasets back into unpaired */
+ _unpair : function( pair, options ){
+ options = options || {};
+ if( !pair ){
+ throw new Error( 'Bad pair: ' + JSON.stringify( pair ) );
+ }
+ this.paired = _.without( this.paired, pair );
+ this._addToUnpaired( pair.forward );
+ this._addToUnpaired( pair.reverse );
+
+ if( !options.silent ){
+ this.trigger( 'pair:unpair', [ pair ] );
+ }
+ return pair;
+ },
+
+ /** unpair all paired datasets */
+ unpairAll : function(){
+ var pairs = [];
+ while( this.paired.length ){
+ pairs.push( this._unpair( this.paired[ 0 ], { silent: true }) );
+ }
+ this.trigger( 'pair:unpair', pairs );
+ },
+
+ // ------------------------------------------------------------------------ API
+ /** convert a pair into JSON compatible with the collections API */
+ _pairToJSON : function( pair, src ){
+ src = src || 'hda';
+ //TODO: consider making this the pair structure when created instead
+ return {
+ collection_type : 'paired',
+ src : 'new_collection',
+ name : pair.name,
+ element_identifiers : [{
+ name : 'forward',
+ id : pair.forward.id,
+ src : src
+ }, {
+ name : 'reverse',
+ id : pair.reverse.id,
+ src : src
+ }]
+ };
+ },
+
+ /** create the collection via the API
+ * @returns {jQuery.xhr Object} the jquery ajax request
+ */
+ createList : function( name ){
+ var creator = this,
+ url = Galaxy.root + 'api/histories/' + this.historyId + '/contents/dataset_collections';
+
+ //TODO: use ListPairedCollection.create()
+ var ajaxData = {
+ type : 'dataset_collection',
+ collection_type : 'list:paired',
+ name : _.escape( name || creator.$( '.collection-name' ).val() ),
+ element_identifiers : creator.paired.map( function( pair ){
+ return creator._pairToJSON( pair );
+ })
+
+ };
+ //this.debug( JSON.stringify( ajaxData ) );
+ creator.blocking = true;
+ return jQuery.ajax( url, {
+ type : 'POST',
+ contentType : 'application/json',
+ dataType : 'json',
+ data : JSON.stringify( ajaxData )
+ })
+ .always( function(){
+ creator.blocking = false;
+ })
+ .fail( function( xhr, status, message ){
+ creator._ajaxErrHandler( xhr, status, message );
+ })
+ .done( function( response, message, xhr ){
+ //this.info( 'ok', response, message, xhr );
+ creator.trigger( 'collection:created', response, message, xhr );
+ creator.metric( 'collection:created', response );
+ if( typeof creator.oncreate === 'function' ){
+ creator.oncreate.call( this, response, message, xhr );
+ }
+ });
+ },
+
+ /** handle ajax errors with feedback and details to the user (if available) */
+ _ajaxErrHandler : function( xhr, status, message ){
+ this.error( xhr, status, message );
+ var content = _l( 'An error occurred while creating this collection' );
+ if( xhr ){
+ if( xhr.readyState === 0 && xhr.status === 0 ){
+ content += ': ' + _l( 'Galaxy could not be reached and may be updating.' )
+ + _l( ' Try again in a few minutes.' );
+ } else if( xhr.responseJSON ){
+ content += '
' + JSON.stringify( xhr.responseJSON ) + '
';
+ } else {
+ content += ': ' + message;
+ }
+ }
+ creator._showAlert( content, 'alert-danger' );
+ },
+
+ // ------------------------------------------------------------------------ rendering
+ /** render the entire interface */
+ render : function( speed, callback ){
+ //this.debug( '-- _render' );
+ //this.$el.empty().html( PairedCollectionCreator.templates.main() );
+ this.$el.empty().html( PairedCollectionCreator.templates.main() );
+ this._renderHeader( speed );
+ this._renderMiddle( speed );
+ this._renderFooter( speed );
+ this._addPluginComponents();
+ this.trigger( 'rendered', this );
+ return this;
+ },
+
+ /** render the header section */
+ _renderHeader : function( speed, callback ){
+ //this.debug( '-- _renderHeader' );
+ var $header = this.$( '.header' ).empty().html( PairedCollectionCreator.templates.header() )
+ .find( '.help-content' ).prepend( $( PairedCollectionCreator.templates.helpContent() ) );
+
+ this._renderFilters();
+ return $header;
+ },
+ /** fill the filter inputs with the filter values */
+ _renderFilters : function(){
+ return this.$( '.forward-column .column-header input' ).val( this.filters[0] )
+ .add( this.$( '.reverse-column .column-header input' ).val( this.filters[1] ) );
+ },
+
+ /** render the middle including unpaired and paired sections (which may be hidden) */
+ _renderMiddle : function( speed, callback ){
+ var $middle = this.$( '.middle' ).empty().html( PairedCollectionCreator.templates.middle() );
+
+ // (re-) hide the un/paired panels based on instance vars
+ if( this.unpairedPanelHidden ){
+ this.$( '.unpaired-columns' ).hide();
+ } else if( this.pairedPanelHidden ){
+ this.$( '.paired-columns' ).hide();
+ }
+
+ this._renderUnpaired();
+ this._renderPaired();
+ return $middle;
+ },
+ /** render the unpaired section, showing datasets accrd. to filters, update the unpaired counts */
+ _renderUnpaired : function( speed, callback ){
+ //this.debug( '-- _renderUnpaired' );
+ var creator = this,
+ $fwd, $rev, $prd = [],
+ split = this._splitByFilters();
+ // update unpaired counts
+ this.$( '.forward-column .title' )
+ .text([ split[0].length, _l( 'unpaired forward' ) ].join( ' ' ));
+ this.$( '.forward-column .unpaired-info' )
+ .text( this._renderUnpairedDisplayStr( this.unpaired.length - split[0].length ) );
+ this.$( '.reverse-column .title' )
+ .text([ split[1].length, _l( 'unpaired reverse' ) ].join( ' ' ));
+ this.$( '.reverse-column .unpaired-info' )
+ .text( this._renderUnpairedDisplayStr( this.unpaired.length - split[1].length ) );
+
+ this.$( '.unpaired-columns .column-datasets' ).empty();
+
+ // show/hide the auto pair button if any unpaired are left
+ this.$( '.autopair-link' ).toggle( this.unpaired.length !== 0 );
+ if( this.unpaired.length === 0 ){
+ this._renderUnpairedEmpty();
+ return;
+ }
+
+ // create the dataset dom arrays
+ $rev = split[1].map( function( dataset, i ){
+ // if there'll be a fwd dataset across the way, add a button to pair the row
+ if( ( split[0][ i ] !== undefined )
+ && ( split[0][ i ] !== dataset ) ){
+ $prd.push( creator._renderPairButton() );
+ }
+ return creator._renderUnpairedDataset( dataset );
+ });
+ $fwd = split[0].map( function( dataset ){
+ return creator._renderUnpairedDataset( dataset );
+ });
+
+ if( !$fwd.length && !$rev.length ){
+ this._renderUnpairedNotShown();
+ return;
+ }
+ // add to appropo cols
+ //TODO: not the best way to render - consider rendering the entire unpaired-columns section in a fragment
+ // and swapping out that
+ this.$( '.unpaired-columns .forward-column .column-datasets' ).append( $fwd )
+ .add( this.$( '.unpaired-columns .paired-column .column-datasets' ).append( $prd ) )
+ .add( this.$( '.unpaired-columns .reverse-column .column-datasets' ).append( $rev ) );
+ this._adjUnpairedOnScrollbar();
+ },
+ /** return a string to display the count of filtered out datasets */
+ _renderUnpairedDisplayStr : function( numFiltered ){
+ return [ '(', numFiltered, ' ', _l( 'filtered out' ), ')' ].join('');
+ },
+ /** return an unattached jQuery DOM element to represent an unpaired dataset */
+ _renderUnpairedDataset : function( dataset ){
+ //TODO: to underscore template
+ return $( '')
+ .attr( 'id', 'dataset-' + dataset.id )
+ .addClass( 'dataset unpaired' )
+ .attr( 'draggable', true )
+ .addClass( dataset.selected? 'selected': '' )
+ .append( $( '' ).addClass( 'dataset-name' ).text( dataset.name ) )
+ //??
+ .data( 'dataset', dataset );
+ },
+ /** render the button that may go between unpaired datasets, allowing the user to pair a row */
+ _renderPairButton : function(){
+ //TODO: *not* a dataset - don't pretend like it is
+ return $( '').addClass( 'dataset unpaired' )
+ .append( $( '' ).addClass( 'dataset-name' ).text( _l( 'Pair these datasets' ) ) );
+ },
+ /** a message to display when no unpaired left */
+ _renderUnpairedEmpty : function(){
+ //this.debug( '-- renderUnpairedEmpty' );
+ var $msg = $( '' )
+ .text( '(' + _l( 'no remaining unpaired datasets' ) + ')' );
+ this.$( '.unpaired-columns .paired-column .column-datasets' ).empty().prepend( $msg );
+ return $msg;
+ },
+ /** a message to display when no unpaired can be shown with the current filters */
+ _renderUnpairedNotShown : function(){
+ //this.debug( '-- renderUnpairedEmpty' );
+ var $msg = $( '' )
+ .text( '(' + _l( 'no datasets were found matching the current filters' ) + ')' );
+ this.$( '.unpaired-columns .paired-column .column-datasets' ).empty().prepend( $msg );
+ return $msg;
+ },
+ /** try to detect if the unpaired section has a scrollbar and adjust left column for better centering of all */
+ _adjUnpairedOnScrollbar : function(){
+ var $unpairedColumns = this.$( '.unpaired-columns' ).last(),
+ $firstDataset = this.$( '.unpaired-columns .reverse-column .dataset' ).first();
+ if( !$firstDataset.size() ){ return; }
+ var ucRight = $unpairedColumns.offset().left + $unpairedColumns.outerWidth(),
+ dsRight = $firstDataset.offset().left + $firstDataset.outerWidth(),
+ rightDiff = Math.floor( ucRight ) - Math.floor( dsRight );
+ //this.debug( 'rightDiff:', ucRight, '-', dsRight, '=', rightDiff );
+ this.$( '.unpaired-columns .forward-column' )
+ .css( 'margin-left', ( rightDiff > 0 )? rightDiff: 0 );
+ },
+
+ /** render the paired section and update counts of paired datasets */
+ _renderPaired : function( speed, callback ){
+ //this.debug( '-- _renderPaired' );
+ this.$( '.paired-column-title .title' ).text([ this.paired.length, _l( 'paired' ) ].join( ' ' ) );
+ // show/hide the unpair all link
+ this.$( '.unpair-all-link' ).toggle( this.paired.length !== 0 );
+ if( this.paired.length === 0 ){
+ this._renderPairedEmpty();
+ return;
+ //TODO: would be best to return here (the $columns)
+ } else {
+ // show/hide 'remove extensions link' when any paired and they seem to have extensions
+ this.$( '.remove-extensions-link' ).show();
+ }
+
+ this.$( '.paired-columns .column-datasets' ).empty();
+ var creator = this;
+ this.paired.forEach( function( pair, i ){
+ //TODO: cache these?
+ var pairView = new PairView({ pair: pair });
+ creator.$( '.paired-columns .column-datasets' )
+ .append( pairView.render().$el )
+ .append([
+ ''
+ ].join( '' ));
+ });
+ },
+ /** a message to display when none paired */
+ _renderPairedEmpty : function(){
+ var $msg = $( '' )
+ .text( '(' + _l( 'no paired datasets yet' ) + ')' );
+ this.$( '.paired-columns .column-datasets' ).empty().prepend( $msg );
+ return $msg;
+ },
+
+ /** render the footer, completion controls, and cancel controls */
+ _renderFooter : function( speed, callback ){
+ var $footer = this.$( '.footer' ).empty().html( PairedCollectionCreator.templates.footer() );
+ this.$( '.remove-extensions' ).prop( 'checked', this.removeExtensions );
+ if( typeof this.oncancel === 'function' ){
+ this.$( '.cancel-create.btn' ).show();
+ }
+ return $footer;
+ },
+
+ /** add any jQuery/bootstrap/custom plugins to elements rendered */
+ _addPluginComponents : function(){
+ this._chooseFiltersPopover( '.choose-filters-link' );
+ this.$( '.help-content i' ).hoverhighlight( '.collection-creator', 'rgba( 64, 255, 255, 1.0 )' );
+ },
+
+ /** build a filter selection popover allowing selection of common filter pairs */
+ _chooseFiltersPopover : function( selector ){
+ function filterChoice( val1, val2 ){
+ return [
+ ''
+ ].join('');
+ }
+ var $popoverContent = $( _.template([
+ '
',
+ '
',
+ _l( 'Choose from the following filters to change which unpaired reads are shown in the display' ),
+ ':