diff --git a/client/galaxy/scripts/layout/scratchbook.js b/client/galaxy/scripts/layout/scratchbook.js index 656c1ac6dc49..6ec1d3703e5f 100644 --- a/client/galaxy/scripts/layout/scratchbook.js +++ b/client/galaxy/scripts/layout/scratchbook.js @@ -41,45 +41,82 @@ return Backbone.View.extend({ }).on( 'show hide ', function() { self.buttonLoad.set( { 'toggle': this.visible, 'icon': this.visible && 'fa-eye' || 'fa-eye-slash' } ); }); + this.history_cache = {}; }, /** Add a dataset to the frames */ addDataset: function( dataset_id ) { + var self = this; + var current_dataset = null; + if ( Galaxy && Galaxy.currHistoryPanel ) { + var history_id = Galaxy.currHistoryPanel.collection.historyId; + this.history_cache[ history_id ] = { name: Galaxy.currHistoryPanel.model.get( 'name' ), dataset_ids: [] }; + Galaxy.currHistoryPanel.collection.each( function( model ) { + !model.get( 'deleted' ) && model.get( 'visible' ) && self.history_cache[ history_id ].dataset_ids.push( model.get( 'id' ) ); + }); + } + var _findDataset = function( dataset, offset ) { + if ( dataset ) { + var history_details = self.history_cache[ dataset.get( 'history_id' ) ]; + if ( history_details && history_details.dataset_ids ) { + var dataset_list = history_details.dataset_ids; + var pos = dataset_list.indexOf( dataset.get( 'id' ) ); + if ( pos !== -1 && pos + offset >= 0 && pos + offset < dataset_list.length ) { + return dataset_list[ pos + offset ]; + } + } + } + }; + var _loadDatasetOffset = function( dataset, offset, frame ) { + var new_dataset_id = _findDataset( dataset, offset ); + if ( new_dataset_id ) { + self._loadDataset( new_dataset_id, function( new_dataset, config ) { + current_dataset = new_dataset; + frame.model.set( config ); + }); + } else { + frame.model.trigger( 'change' ); + } + } + this._loadDataset( dataset_id, function( dataset, config ) { + current_dataset = dataset; + self.add( _.extend( { menu: [ { icon : 'fa fa-chevron-circle-left', + tooltip : 'Previous in History', + onclick : function( frame ) { _loadDatasetOffset( current_dataset, -1, frame ) }, + disabled : function() { return !_findDataset( current_dataset, -1 ) } }, + { icon : 'fa fa-chevron-circle-right', + tooltip : 'Next in History', + onclick : function( frame ) { _loadDatasetOffset( current_dataset, 1, frame ) }, + disabled : function() { return !_findDataset( current_dataset, 1 ) } } ] }, config ) ) + }); + }, + + _loadDataset: function( dataset_id, callback ) { var self = this; require([ 'mvc/dataset/data' ], function( DATA ) { var dataset = new DATA.Dataset( { id : dataset_id } ); $.when( dataset.fetch() ).then( function() { - // Construct frame config based on dataset's type. - var frame_config = { - title: dataset.get('name') - }, - // HACK: For now, assume 'tabular' and 'interval' are the only - // modules that contain tabular files. This needs to be replaced - // will a is_datatype() function. - is_tabular = _.find( [ 'tabular', 'interval' ] , function( data_type ) { - return dataset.get( 'data_type' ).indexOf( data_type ) !== -1; - }); - - // Use tabular chunked display if dataset is tabular; otherwise load via URL. - if ( is_tabular ) { - var tabular_dataset = new DATA.TabularDataset( dataset.toJSON() ); - _.extend( frame_config, { - content: function( parent_elt ) { - DATA.createTabularDatasetChunkedView({ - model : tabular_dataset, - parent_elt : parent_elt, - embedded : true, - height : '100%' - }); - } - }); - } - else { - _.extend( frame_config, { - url: Galaxy.root + 'datasets/' + dataset.id + '/display/?preview=True' - }); + var is_tabular = _.find( [ 'tabular', 'interval' ] , function( data_type ) { + return dataset.get( 'data_type' ).indexOf( data_type ) !== -1; + }); + var title = dataset.get( 'name' ); + var history_details = self.history_cache[ dataset.get( 'history_id' ) ]; + if ( history_details ) { + title = history_details.name + ': ' + title; } - self.add( frame_config ); + callback( dataset, is_tabular ? { + title : title, + url : null, + content : DATA.createTabularDatasetChunkedView({ + model : new DATA.TabularDataset( dataset.toJSON() ), + embedded : true, + height : '100%' + }).$el + } : { + title : title, + url : Galaxy.root + 'datasets/' + dataset_id + '/display/?preview=True', + content : null + } ); }); }); }, @@ -136,15 +173,9 @@ return Backbone.View.extend({ window.location = options.url; } else if ( !this.active ) { var $galaxy_main = $( window.parent.document ).find( '#galaxy_main' ); - if ( options.target == 'galaxy_main' || options.target == 'center' ){ - if ( $galaxy_main.length === 0 ){ - var href = options.url; - if ( href.indexOf( '?' ) == -1 ) - href += '?'; - else - href += '&'; - href += 'use_panels=True'; - window.location = href; + if ( options.target == 'galaxy_main' || options.target == 'center' ) { + if ( $galaxy_main.length === 0 ) { + window.location = options.url + ( href.indexOf( '?' ) == -1 ? '?' : '&' ) + 'use_panels=True'; } else { $galaxy_main.attr( 'src', options.url ); } diff --git a/client/galaxy/scripts/mvc/history/history-model.js b/client/galaxy/scripts/mvc/history/history-model.js index a29f2eca576c..863f6e722207 100644 --- a/client/galaxy/scripts/mvc/history/history-model.js +++ b/client/galaxy/scripts/mvc/history/history-model.js @@ -435,6 +435,7 @@ var ControlledFetchMixin = { _.each( filters, function( v, k ){ if( v === true ){ v = 'True'; } if( v === false ){ v = 'False'; } + if( v === null ){ v = 'None'; } filterMap.q.push( k ); filterMap.qv.push( v ); }); @@ -522,6 +523,11 @@ var HistoryCollection = Backbone.Collection deleted : false, purged : false, }; + } else { + defaults.filters = { + // TODO: for bypassing defaults on current API + deleted : null, + }; } return defaults; }, diff --git a/client/galaxy/scripts/mvc/tool/tool-form-composite.js b/client/galaxy/scripts/mvc/tool/tool-form-composite.js index 816110511b18..15b122e51fc4 100644 --- a/client/galaxy/scripts/mvc/tool/tool-form-composite.js +++ b/client/galaxy/scripts/mvc/tool/tool-form-composite.js @@ -1,46 +1,40 @@ -/** - This is the run workflow tool form. -*/ -define([ 'utils/utils', 'mvc/ui/ui-misc', 'mvc/form/form-view', 'mvc/form/form-data', 'mvc/tool/tool-form-base' ], - function( Utils, Ui, Form, FormData, ToolFormBase ) { +/** This is the run workflow tool form view. */ +define([ 'utils/utils', 'utils/deferred', 'mvc/ui/ui-misc', 'mvc/form/form-view', 'mvc/form/form-data', 'mvc/tool/tool-form-base' ], + function( Utils, Deferred, Ui, Form, FormData, ToolFormBase ) { var View = Backbone.View.extend({ initialize: function( options ) { + this.model = options && options.model || new Backbone.Model( options ); + this.deferred = new Deferred(); + this.setElement( $( '
' ).addClass( 'ui-form-composite' ) + .append( this.$message = $( '' ) ) + .append( this.$header = $( '' ) ) + .append( this.$parameters = $( '' ) ) + .append( this.$steps = $( '' ) ) + .append( this.$history = $( '' ) ) + .append( this.$execute = $( '' ) ) ); + $( 'body' ).append( this.$el ); + this._configure(); + this.render(); + }, + + /** Configures form/step options for each workflow step */ + _configure: function() { var self = this; - this.workflow_id = options.id; this.forms = []; this.steps = []; this.links = []; - - // initialize elements - this.setElement( '' ); - this.$header = $( '' ).addClass( 'ui-form-header' ); - this.$header.append( new Ui.Label({ - title : 'Workflow: ' + options.name - }).$el ); - this.$header.append( new Ui.Button({ - title : 'Collapse', - icon : 'fa-angle-double-up', - onclick : function() { _.each( self.forms, function( form ) { form.portlet.collapse() }) } - }).$el ); - this.$header.append( new Ui.Button({ - title : 'Expand all', - icon : 'fa-angle-double-down', - onclick : function() { _.each( self.forms, function( form ) { form.portlet.expand() }) } - }).$el ); - this.$el.append( this.$header ); - - // initialize steps and configure connections - _.each( options.steps, function( step, i ) { + this.parms = []; + _.each( this.model.get( 'steps' ), function( step, i ) { Galaxy.emit.debug( 'tool-form-composite::initialize()', i + ' : Preparing workflow step.' ); step = Utils.merge( { + index : i, name : 'Step ' + ( parseInt( i ) + 1 ) + ': ' + step.name, icon : '', help : null, description : step.annotation && ' - ' + step.annotation || step.description, citations : null, - needs_update : true, collapsible : true, - collapsed : i > 0, + collapsed : i > 0 && !self._isDataStep( step ), sustain_version : true, sustain_repeats : true, sustain_conditionals : true, @@ -48,155 +42,256 @@ define([ 'utils/utils', 'mvc/ui/ui-misc', 'mvc/form/form-view', 'mvc/form/form-d text_enable : 'Edit', text_disable : 'Undo', cls_enable : 'fa fa-edit', - cls_disable : 'fa fa-undo' + cls_disable : 'fa fa-undo', + errors : step.messages, + initial_errors : true }, step ); - // convert all connected data inputs to hidden fields with proper labels - _.each( options.steps, function( sub_step ) { + self.steps[ i ] = step; + self.links[ i ] = []; + self.parms[ i ] = {} + }); + + // build linear index of step input pairs + _.each( this.steps, function( step, i ) { + FormData.visitInputs( step.inputs, function( input, name ) { + self.parms[ i ][ name ] = input; + }); + }); + + // iterate through data input modules and collect linked sub steps + _.each( this.steps, function( step, i ) { + _.each( step.output_connections, function( output_connection ) { + _.each( self.steps, function( sub_step, j ) { + sub_step.step_id === output_connection.input_step_id && self.links[ i ].push( sub_step ); + }); + }); + }); + + // convert all connected data inputs to hidden fields with proper labels, + // and track the linked source step + _.each( this.steps, function( step, i ) { + _.each( self.steps, function( sub_step, j ) { var connections_by_name = {}; _.each( step.output_connections, function( connection ) { sub_step.step_id === connection.input_step_id && ( connections_by_name[ connection.input_name ] = connection ); }); - FormData.matchIds( sub_step.inputs, connections_by_name, function( connection, input ) { - if ( !input.linked ) { - input.linked = step.step_type; + _.each( self.parms[ j ], function( input, name ) { + var connection = connections_by_name[ name ]; + if ( connection ) { input.type = 'hidden'; - input.help = ''; - } else { - input.help += ', '; + input.help = input.step_linked ? input.help + ', ' : ''; + input.help += 'Output dataset \'' + connection.output_name + '\' from step ' + ( parseInt( i ) + 1 ); + input.step_linked = input.step_linked || []; + input.step_linked.push( step ); } - input.help += 'Output dataset \'' + connection.output_name + '\' from step ' + ( parseInt( i ) + 1 ); }); }); - self.steps[ i ] = step; - self.links[ i ] = []; }); - // finalize configuration and build forms - _.each( self.steps, function( step, i ) { - var form = null; - if ( String( step.step_type ).startsWith( 'data' ) ) { - form = new Form( Utils.merge({ - title : '' + step.name + '', - onchange: function() { - var input_value = form.data.create().input; - _.each( self.links[ i ], function( link ) { - link.input.value ( input_value ); - link.form.trigger( 'change' ); - }); - } - }, step )); - } else if ( step.step_type == 'tool' ) { - // select fields are shown for dynamic fields if all putative data inputs are available - function visitInputs( inputs, data_resolved ) { - data_resolved === undefined && ( data_resolved = true ); - _.each( inputs, function ( input ) { - if ( _.isObject( input ) ) { - if ( input.type ) { - var is_data_input = [ 'data', 'data_collection' ].indexOf( input.type ) !== -1; - var is_workflow_parameter = self._isWorkflowParameter( input.value ); - is_data_input && input.linked && !input.linked.startsWith( 'data' ) && ( data_resolved = false ); - input.options && ( ( input.options.length == 0 && !data_resolved ) || is_workflow_parameter ) && ( input.is_workflow = true ); - input.value && input.value.__class__ == 'RuntimeValue' && ( input.value = null ); - if ( !is_data_input && input.type !== 'hidden' && !is_workflow_parameter ) { - if ( input.optional || ( !Utils.isEmpty( input.value ) && input.value !== '' ) ) { - input.collapsible_value = input.value; - input.collapsible_preview = true; - } - } - } - visitInputs( input, data_resolved ); - } - }); - }; - visitInputs( step.inputs ); - // or if a particular reference is specified and available - FormData.matchContext( step.inputs, 'data_ref', function( input, reference ) { - input.is_workflow = ( reference.linked && !reference.linked.startsWith( 'data' ) ) || self._isWorkflowParameter( input.value ); + // identify and configure workflow parameters + var wp_count = 0; + this.wp_inputs = {}; + function _handleWorkflowParameter( value, callback ) { + var wp_name = self._isWorkflowParameter( value ); + wp_name && callback( self.wp_inputs[ wp_name ] = self.wp_inputs[ wp_name ] || { + label : wp_name, + name : wp_name, + type : 'text', + color : 'hsl( ' + ( ++wp_count * 100 ) + ', 70%, 30% )', + style : 'ui-form-wp-source', + links : [] + }); + } + _.each( this.steps, function( step, i ) { + _.each( self.parms[ i ], function( input, name ) { + _handleWorkflowParameter( input.value, function( wp_input ) { + wp_input.links.push( step ); + input.wp_linked = wp_input.name; + input.color = wp_input.color; + input.type = 'text'; + input.value = null; + input.backdrop = true; + input.style = 'ui-form-wp-target'; }); - form = new ToolFormBase( step ); - } - self.forms[ i ] = form; + }); + _.each( step.post_job_actions, function( pja ) { + _.each( pja.action_arguments, function( arg ) { + _handleWorkflowParameter( arg, function() {} ); + }); + }); }); - // create index of data output links + // select fields are shown for dynamic fields if all putative data inputs are available, + // or if an explicit reference is specified as data_ref and available _.each( this.steps, function( step, i ) { - _.each( step.output_connections, function( output_connection ) { - _.each( self.forms, function( form ) { - if ( form.options.step_id === output_connection.input_step_id ) { - var matched_input = form.field_list[ form.data.match( output_connection.input_name ) ]; - matched_input && self.links[ i ].push( { input: matched_input, form: form } ); + if ( step.step_type == 'tool' ) { + var data_resolved = true; + FormData.visitInputs( step.inputs, function ( input, name, context ) { + var is_data_input = ([ 'data', 'data_collection' ]).indexOf( input.type ) != -1; + var data_ref = context[ input.data_ref ]; + input.step_linked && !self._isDataStep( input.step_linked ) && ( data_resolved = false ); + input.options && ( ( input.options.length == 0 && !data_resolved ) || input.wp_linked ) && ( input.is_workflow = true ); + data_ref && ( input.is_workflow = ( data_ref.step_linked && !self._isDataStep( data_ref.step_linked ) ) || input.wp_linked ); + ( is_data_input || ( input.value && input.value.__class__ == 'RuntimeValue' && !input.step_linked ) ) && ( step.collapsed = false ); + input.value && input.value.__class__ == 'RuntimeValue' && ( input.value = null ); + input.flavor = 'workflow'; + if ( !is_data_input && input.type !== 'hidden' && !input.wp_linked ) { + if ( input.optional || ( !Utils.isEmpty( input.value ) && input.value !== '' ) ) { + input.collapsible_value = input.value; + input.collapsible_preview = true; + } } }); - }); + } }); + }, - // build workflow parameters - var wp_fields = {}; - var wp_inputs = {}; - var wp_count = 0; - var wp_style = function( wp_field, wp_color, wp_cls ) { - var $wp_input = wp_field.$( 'input' ); - $wp_input.length === 0 && ( $wp_input = wp_field.$el ); - $wp_input.addClass( wp_cls ).css({ 'color': wp_color, 'border-color': wp_color }); + render: function() { + var self = this; + this.deferred.reset(); + this._renderHeader(); + this._renderMessage(); + this._renderParameters(); + this._renderHistory(); + _.each ( this.steps, function( step, i ) { self._renderStep( step, i ) } ); + this.deferred.execute( function() { self._renderExecute() } ); + }, + + /** Render header */ + _renderHeader: function() { + var self = this; + this.$header.addClass( 'ui-form-header' ).empty() + .append( new Ui.Label({ + title : 'Workflow: ' + this.model.get( 'name' ) }).$el ) + .append( new Ui.Button({ + title : 'Collapse', + icon : 'fa-angle-double-up', + onclick : function() { _.each( self.forms, function( form ) { form.portlet.collapse() }) } }).$el ) + .append( new Ui.Button({ + title : 'Expand all', + icon : 'fa-angle-double-down', + onclick : function() { _.each( self.forms, function( form ) { form.portlet.expand() }) } }).$el ); + }, + + /** Render message */ + _renderMessage: function() { + this.$message.empty(); + if ( this.model.get( 'has_upgrade_messages' ) ) { + this.$message.append( new Ui.Message( { + message : 'Some tools in this workflow may have changed since it was last saved or some errors were found. The workflow may still run, but any new options will have default values. Please review the messages below to make a decision about whether the changes will affect your analysis.', + status : 'warning', + persistent : true + } ).$el ); } - _.each( this.steps, function( step, i ) { - _.each( step.inputs, function( input ) { - var wp_name = self._isWorkflowParameter( input.value ); - if ( wp_name ) { - var wp_field = self.forms[ i ].field_list[ input.id ]; - var wp_element = self.forms[ i ].element_list[ input.id ]; - wp_fields[ wp_name ] = wp_fields[ wp_name ] || []; - wp_fields[ wp_name ].push( wp_field ); - wp_field.value( wp_name ); - wp_element.disable( true ); - wp_inputs[ wp_name ] = wp_inputs[ wp_name ] || { - type : input.type, - is_workflow : input.options, - label : wp_name, - name : wp_name, - color : 'hsl( ' + ( ++wp_count * 100 ) + ', 70%, 30% )' - }; - wp_style( wp_field, wp_inputs[ wp_name ].color, 'ui-form-wp-target' ); - } - }); - }); - if ( !_.isEmpty( wp_inputs ) ) { - var wp_form = new Form({ title: 'Workflow Parameters', inputs: wp_inputs, onchange: function() { - _.each( wp_form.data.create(), function( wp_value, wp_name ) { - _.each( wp_fields[ wp_name ], function( wp_field ) { - wp_field.value( Utils.sanitize( wp_value ) || wp_name ); + }, + + /** Render workflow parameters */ + _renderParameters: function() { + var self = this; + this.wp_form = null; + if ( !_.isEmpty( this.wp_inputs ) ) { + this.wp_form = new Form({ title: 'Workflow Parameters', inputs: this.wp_inputs, onchange: function() { + _.each( self.wp_form.input_list, function( input_def, i ) { + _.each( input_def.links, function( step ) { + self._refreshStep( step ); }); }); }}); - _.each( wp_form.field_list, function( wp_field, i ) { - wp_style( wp_field, wp_form.input_list[ i ].color, 'ui-form-wp-source' ); - }); - this.$el.append( '' ).addClass( 'ui-margin-top' ); - this.$el.append( wp_form.$el ); + this._append( this.$parameters.empty(), this.wp_form.$el ); } + }, - // append elements - _.each( this.steps, function( step, i ) { - var form = self.forms[ i ]; - self.$el.append( '' ).addClass( 'ui-margin-top' ).append( form.$el ); - if ( step.post_job_actions && step.post_job_actions.length ) { - form.portlet.append( $( '' ).addClass( 'ui-form-footer-info fa fa-bolt' ).append( - _.reduce( step.post_job_actions, function( memo, value ) { - return memo + ' ' + value.short_str; - }, '' )) - ); + /** Render step */ + _renderStep: function( step, i ) { + var self = this; + var form = null; + var current = null; + this.deferred.execute( function( promise ) { + current = promise; + if ( self._isDataStep( step ) ) { + _.each( step.inputs, function( input ) { input.flavor = 'module' } ); + form = new Form( Utils.merge({ + title : '' + step.name + '', + onchange : function() { _.each( self.links[ i ], function( link ) { self._refreshStep( link ) } ) } + }, step ) ); + self._append( self.$steps, form.$el ); + } else if ( step.step_type == 'tool' ) { + form = new ToolFormBase( step ); + if ( step.post_job_actions && step.post_job_actions.length ) { + form.portlet.append( $( '' ).addClass( 'ui-form-element-disabled' ) + .append( $( '' ).addClass( 'ui-form-title' ).html( 'Job Post Actions' ) ) + .append( $( '' ).addClass( 'ui-form-preview' ).html( + _.reduce( step.post_job_actions, function( memo, value ) { + return memo + ' ' + value.short_str; + }, '' ) ) ) + ); + } + self._append( self.$steps, form.$el ); } + self.forms[ i ] = form; + self._refreshStep( step ); Galaxy.emit.debug( 'tool-form-composite::initialize()', i + ' : Workflow step state ready.', step ); - }); + self._resolve( form.deferred, promise ); + } ); + }, + + /** This helps with rendering lazy loaded steps */ + _resolve: function( deferred, promise ) { + var self = this; + setTimeout( function() { + if ( deferred && deferred.ready() || !deferred ) { + promise.resolve(); + } else { + self._resolve( deferred, promise ); + } + }, 0 ); + }, + + /** Refreshes step values from source step values */ + _refreshStep: function( step ) { + var self = this; + var form = this.forms[ step.index ]; + if ( form ) { + _.each( self.parms[ step.index ], function( input, name ) { + if ( input.step_linked || input.wp_linked ) { + var field = form.field_list[ form.data.match( name ) ]; + if ( field ) { + var new_value = undefined; + if ( input.step_linked ) { + new_value = { values: [] }; + _.each( input.step_linked, function( source_step ) { + if ( self._isDataStep( source_step ) ) { + value = self.forms[ source_step.index ].data.create().input; + value && _.each( value.values, function( v ) { new_value.values.push( v ) } ); + } + }); + if ( !input.multiple && new_value.values.length > 0 ) { + new_value = { values: [ new_value.values[ 0 ] ] }; + } + } else if ( input.wp_linked ) { + var wp_field = self.wp_form.field_list[ self.wp_form.data.match( input.wp_linked ) ]; + wp_field && ( new_value = wp_field.value() ); + } + if ( new_value !== undefined ) { + field.value( new_value ); + } + } + } + }); + form.trigger( 'change' ); + } + }, - // add history form + /** Render history form */ + _renderHistory: function() { this.history_form = null; - if ( !options.history_id ) { + if ( !this.model.get( 'history_id' ) ) { this.history_form = new Form({ inputs : [{ type : 'conditional', + name : 'new_history', test_param : { - name : 'new_history', + name : 'check', label : 'Send results to a new history', type : 'boolean', value : 'false', @@ -205,20 +300,21 @@ define([ 'utils/utils', 'mvc/ui/ui-misc', 'mvc/form/form-view', 'mvc/form/form-d cases : [{ value : 'true', inputs : [{ - name : 'new_history_name', + name : 'name', label : 'History name', type : 'text', - value : options.name + value : this.model.get( 'name' ) }] }] }] }); - this.$el.append( '' ).addClass( 'ui-margin-top' ); - this.$el.append( this.history_form.$el ); + this._append( this.$history.empty(), this.history_form.$el ); } + }, - // add execute button - this.$el.append( '' ).addClass( 'ui-margin-top' ); + /** Render execute button */ + _renderExecute: function() { + var self = this; this.execute_btn = new Ui.Button({ icon : 'fa-check', title : 'Run workflow', @@ -226,81 +322,84 @@ define([ 'utils/utils', 'mvc/ui/ui-misc', 'mvc/form/form-view', 'mvc/form/form-d floating : 'clear', onclick : function() { self._execute() } }); - this.$el.append( this.execute_btn.$el ); - $( 'body' ).append( this.$el ); + this._append( this.$execute.empty(), this.execute_btn.$el ); }, - /** Execute workflow - */ + /** Execute workflow */ _execute: function() { var self = this; var job_def = { - inputs : {}, - parameters : {} + new_history_name : this.history_form.data.create()[ 'new_history|name' ], + wf_parm : this.wp_form ? this.wp_form.data.create() : {}, + inputs : {} }; var validated = true; - _.each( this.forms, function( form, i ) { + for ( var i in this.forms ) { + var form = this.forms[ i ]; var job_inputs = form.data.create(); var step = self.steps[ i ]; var step_id = step.step_id; - var step_type = step.step_type; - var order_index = step.order_index; - job_def.parameters[ step_id ] = {}; form.trigger( 'reset' ); for ( var job_input_id in job_inputs ) { var input_value = job_inputs[ job_input_id ]; var input_id = form.data.match( job_input_id ); var input_field = form.field_list[ input_id ]; var input_def = form.input_list[ input_id ]; - if ( String( step_type ).startsWith( 'data' ) ) { - if ( input_value && input_value.values && input_value.values.length > 0 ) { - job_def.inputs[ order_index ] = input_value.values[ 0 ]; - } else if ( validated ) { - form.highlight( input_id ); - validated = false; + if ( !input_def.step_linked ) { + if ( this._isDataStep( step ) ) { + validated = input_value && input_value.values && input_value.values.length > 0; + } else { + validated = input_def.optional || ( input_def.is_workflow && input_value !== '' ) || ( !input_def.is_workflow && input_value !== null ); } - } else { - if ( !String( input_def.type ).startsWith( 'data' ) ) { - if ( input_def.optional || input_def.is_workflow || input_value != null ) { - job_def.parameters[ step_id ][ job_input_id ] = input_value; - } else { - form.highlight( input_id ); - validated = false; - } + if ( !validated ) { + form.highlight( input_id ); + break; } + job_def.inputs[ step_id ] = job_def.inputs[ step_id ] || {}; + job_def.inputs[ step_id ][ job_input_id ] = job_inputs[ job_input_id ]; } } - }); - console.log( JSON.stringify( job_def ) ); + if ( !validated ) { + break; + } + } if ( !validated ) { self._enabled( true ); + Galaxy.emit.debug( 'tool-form-composite::submit()', 'Validation failed.', job_def ); } else { self._enabled( false ); - Galaxy.emit.debug( 'tools-form-composite::submit()', 'Validation complete.', job_def ); + Galaxy.emit.debug( 'tool-form-composite::submit()', 'Validation complete.', job_def ); Utils.request({ type : 'POST', url : Galaxy.root + 'api/workflows/' + this.workflow_id + '/invocations', data : job_def, success : function( response ) { Galaxy.emit.debug( 'tool-form-composite::submit', 'Submission successful.', response ); + self.$el.empty().append( self._templateSuccess( response ) ); parent.Galaxy && parent.Galaxy.currHistoryPanel && parent.Galaxy.currHistoryPanel.refreshContents(); - console.log( response ); }, error : function( response ) { - console.log( response ); + Galaxy.emit.debug( 'tool-form-composite::submit', 'Submission failed.', response ); if ( response && response.err_data ) { - var error_messages = form.data.matchResponse( response.err_data ); - for ( var input_id in error_messages ) { - form.highlight( input_id, error_messages[ input_id ] ); - break; + for ( var i in self.forms ) { + var form = self.forms[ i ]; + var step_related_errors = response.err_data[ form.options.step_id ]; + if ( step_related_errors ) { + var error_messages = form.data.matchResponse( step_related_errors ); + for ( var input_id in error_messages ) { + form.highlight( input_id, error_messages[ input_id ] ); + break; + } + } } } else { - Galaxy.modal && Galaxy.modal.show({ + var modal = parent.Galaxy.modal; + modal && modal.show({ title : 'Job submission failed', - body : ( response && response.err_msg ) || ToolTemplate.error( options.job_def ), + body : self._templateError( response && response.err_msg || job_def ), buttons : { 'Close' : function() { - Galaxy.modal.hide(); + modal.hide(); } } }); @@ -313,23 +412,69 @@ define([ 'utils/utils', 'mvc/ui/ui-misc', 'mvc/form/form-view', 'mvc/form/form-d } }, - /** Set enabled/disabled state - */ + /** Append new dom to body */ + _append: function( $container, $el ) { + $container.append( '' ).addClass( 'ui-margin-top' ).append( $el ); + }, + + /** Set enabled/disabled state */ _enabled: function( enabled ) { if ( enabled ) { this.execute_btn.unwait() } else { this.execute_btn.wait() } if ( enabled ) { this.history_form.portlet.enable() } else { this.history_form.portlet.disable() } _.each( this.forms, function( form ) { if ( enabled ) { form.portlet.enable() } else { form.portlet.disable() } }); }, - /** Handle workflow parameter - */ + /** Handle workflow parameter */ _isWorkflowParameter: function( value ) { if ( String( value ).substring( 0, 1 ) === '$' ) { return Utils.sanitize( value.substring( 2, value.length - 1 ) ) } + }, + + /** Is data input module/step */ + _isDataStep: function( steps ) { + lst = $.isArray( steps ) ? steps : [ steps ] ; + for ( var i = 0; i < lst.length; i++ ) { + var step = lst[ i ]; + if ( !step || !step.step_type || !step.step_type.startsWith( 'data' ) ) { + return false; + } + } + return true; + }, + + /** Templates */ + _templateSuccess: function( response ) { + if ( response && response.length > 0 ) { + var $message = $( '' ).addClass( 'donemessagelarge' ) + .append( $( '' ).text( 'Successfully ran workflow \'' + this.model.get( 'name' ) + '\'. The following datasets have been added to the queue:' ) ); + for ( var i in response ) { + var invocation = response[ i ]; + var $invocation = $( '' ).addClass( 'workflow-invocation-complete' ); + invocation.history && $invocation.append( $( '' ).text( 'These datasets will appear in a new history: ' ) + .append( $( '' ).addClass( 'new-history-link' ) + .attr( 'data-history-id', invocation.history.id ) + .attr( 'target', '_top' ) + .attr( 'href', '/history/switch_to_history?hist_id=' + invocation.history.id ) + .text( invocation.history.name ) ) ); + _.each( invocation.outputs, function( output ) { + $invocation.append( $( '' ).addClass( 'messagerow' ).html( '' + output.hid + ': ' + output.name ) ); + }); + $message.append( $invocation ); + } + return $message; + } else { + return this._templateError( response ); + } + }, + + _templateError: function( response ) { + return $( '' ).addClass( 'errormessagelarge' ) + .append( $( '' ).text( 'The server could not complete the request. Please contact the Galaxy Team if this error persists.' ) ) + .append( $( '' ).text( JSON.stringify( response, null, 4 ) ) ); } }); return { View: View }; -}); +}); \ No newline at end of file diff --git a/client/galaxy/scripts/mvc/ui/ui-frames.js b/client/galaxy/scripts/mvc/ui/ui-frames.js index 2ea869a2291a..280f1479e2ec 100644 --- a/client/galaxy/scripts/mvc/ui/ui-frames.js +++ b/client/galaxy/scripts/mvc/ui/ui-frames.js @@ -1,50 +1,91 @@ -/** Scratchbook viewer */ define([], function() { + +/** Frame view */ +var FrameView = Backbone.View.extend({ + initialize: function( options ) { + var self = this; + this.model = options && options.model || new Backbone.Model( options ); + this.setElement( $( '' ).addClass( 'corner frame' ) ); + this.$el.append( $( '' ).addClass( 'f-header corner' ) + .append( $( '' ).addClass( 'f-title' ) ) + .append( $( '' ).addClass( 'f-icon f-close fa fa-close' ) + .tooltip( { title: 'Close', placement: 'bottom' } ) ) ) + .append( $( '' ).addClass( 'f-content' ) ) + .append( $( '' ).addClass( 'f-resize f-icon corner fa fa-expand' ).tooltip( { title: 'Resize' } ) ) + .append( $( '' ).addClass( 'f-cover' ) ); + this.$header = this.$( '.f-header' ); + this.$title = this.$( '.f-title' ); + this.$content = this.$( '.f-content' ); + this.render(); + this.listenTo( this.model, 'change', this.render, this ); + }, + + render: function() { + var self = this; + var options = this.model.attributes; + this.$title.html( options.title || '' ); + this.$header.find( '.f-icon-left' ).remove(); + _.each( options.menu, function( option ) { + var $option = $( '' ).addClass( 'f-icon-left' ).addClass( option.icon ); + if ( _.isFunction( option.disabled ) && option.disabled() ) { + $option.attr( 'disabled', true ); + } else { + $option.on( 'click', function() { option.onclick( self ) } ) + .tooltip( { title: option.tooltip, placement: 'bottom' } ); + } + self.$header.append( $option ); + } ); + if ( options.url ) { + this.$content.html( $ ( '' ).addClass( 'f-iframe' ) + .attr( 'scrolling', 'auto' ) + .attr( 'src', options.url + ( options.url.indexOf( '?' ) === -1 ? '?' : '&' ) + 'widget=True' ) ); + } else if ( options.content ) { + _.isFunction( options.content ) ? options.content( self.$content ) : self.$content.html( options.content ); + } + } +}); + +/** Scratchbook viewer */ var View = Backbone.View.extend({ defaultOptions: { - frame: { // default frame size in cells + frame: { // default frame size in cells cols : 6, rows : 3 }, - rows : 1000, // maximum number of rows - cell : 130, // cell size in px - margin : 5, - scroll : 5, // scroll speed - top_min : 40, // top margin - frame_max : 9, // maximum number of frames - visible : true, // initial visibility - }, - - cols : 0, // number of columns - top : 0, // scroll/element top - top_max : 0, // viewport scrolling state - frame_z : 0, // frame z-index - frame_counter : 0, // frame counter - frame_uid : 0, - frame_list : {}, // list of all frames - frame_shadow : null, - visible : false, - event : {}, + rows : 1000, // maximum number of rows + cell : 130, // cell size in px + margin : 5, // margin between frames + scroll : 5, // scroll speed + top_min : 40, // top margin + frame_max : 9, // maximum number of frames + visible : true, // initial visibility + }, + + cols : 0, // number of columns + top : 0, // scroll/element top + top_max : 0, // viewport scrolling state + frame_z : 0, // frame z-index + frame_counter : 0, // frame counter + frame_uid : 0, // unique frame id counter + frame_list : {}, // list of all frames + frame_shadow : null, // frame shown as placeholder when moving active frames + visible : false, // flag indicating if scratchbook viewer is visible or not + event : {}, // dictionary keeping track of current event initialize : function( options ) { var self = this; this.options = _.defaults( options || {}, this.defaultOptions ); this.visible = this.options.visible; this.top = this.top_max = this.options.top_min; - this.setElement( $( '' ).addClass( 'galaxy-frame' ) ); - this.$el.append( $( '' ).addClass( 'frame-background' ) ); - this.$el.append( $( '' ).addClass( 'frame-menu frame-scroll-up fa fa-chevron-up fa-2x' ) ); - this.$el.append( $( '' ).addClass( 'frame-menu frame-scroll-down fa fa-chevron-down fa-2x' ) ); - this.$el.append( $( '' ).addClass( 'frame-shadow corner' ).attr( 'id', 'frame-shadow' ) ); + this.setElement( $( '' ).addClass( 'galaxy-frame' ) + .append( $( '' ).addClass( 'frame-background' ) ) + .append( $( '' ).addClass( 'frame-menu frame-scroll-up fa fa-chevron-up fa-2x' ) ) + .append( $( '' ).addClass( 'frame-menu frame-scroll-down fa fa-chevron-down fa-2x' ) ) ); // initialize shadow to guiding drag/resize events - this.frame_shadow = { - id : '#frame-shadow', - screen_location : {}, - grid_location : {}, - grid_rank : null, - grid_lock : false - }; + this.frame_shadow = new Backbone.View({ el: $( '' ).addClass( 'corner frame-shadow' ) } ); + this.$el.append( this.frame_shadow.$el ); + this._frameInit( this.frame_shadow, '#frame-shadow' ); this._frameResize( this.frame_shadow, { width: 0, height: 0 } ); this.frame_list[ '#frame-shadow' ] = this.frame_shadow; @@ -87,38 +128,18 @@ var View = Backbone.View.extend({ } else { // initialize new frame elements this.top = this.options.top_min; - var $frame_el = $( this._frameTemplate( frame_id.substring( 1 ), options.title ) ); - var $frame_content = $frame_el.find( '.f-content' ); - this.$el.append( $frame_el ); - - // configure content - if ( options.url ) { - $frame_content.append( - $ ( '' ).addClass( 'f-iframe' ) - .attr( 'scrolling', 'auto' ) - .attr( 'src', options.url + ( options.url.indexOf( '?' ) === -1 ? '?' : '&' ) + 'widget=True' ) - ); - } else if ( options.content ) { - _.isFunction( options.content ) ? options.content( $frame_content ) : $frame_content.append( options.content ); - } - - // construct a new frame - var frame = { - id : frame_id, - screen_location : {}, - grid_location : {}, - grid_rank : null, - grid_lock : false - }; + var frame = new FrameView( options ); + this.$el.append( frame.$el ); // set dimensions options.width = this._toPixelCoord( 'width', this.options.frame.cols ); options.height = this._toPixelCoord( 'height', this.options.frame.rows ); // set default z-index and add to ui and frame list - this.frame_z = parseInt( $( frame.id ).css( 'z-index' ) ); + this.frame_z = parseInt( frame.$el.css( 'z-index' ) ); this.frame_list[ frame_id ] = frame; this.frame_counter++; + this._frameInit( frame, frame_id ); this._frameResize( frame, { width: options.width, height: options.height } ); this._frameInsert( frame, { top: 0, left: 0 }, true ); !this.visible && this.show(); @@ -128,12 +149,12 @@ var View = Backbone.View.extend({ }, /** Remove a frame */ - del: function( frame_id ) { + del: function( frame ) { var self = this; - var $frame = this.$( frame_id ); + var $frame = frame.$el; $frame.fadeOut( 'fast', function() { $frame.remove(); - delete self.frame_list[ frame_id ]; + delete self.frame_list[ frame.id ]; self.frame_counter--; self._panelRefresh( true ); self._panelAnimationComplete(); @@ -178,12 +199,12 @@ var View = Backbone.View.extend({ 'mousedown .frame-background' : '_eventHide', 'mousedown .frame-scroll-up' : '_eventPanelScroll_up', 'mousedown .frame-scroll-down' : '_eventPanelScroll_down', - 'mousedown .f-close' : '_eventFrameClose', - 'mousedown .f-pin' : '_eventFrameLock' + 'mousedown .f-close' : '_eventFrameClose' }, /** Start drag/resize event */ _eventFrameMouseDown: function ( e ) { + $( '.tooltip' ).hide(); if ( !this.event.type ) { if ( $( e.target ).hasClass( 'f-header' ) || $( e.target ).hasClass( 'f-title' ) ) { this.event.type = 'drag'; @@ -194,10 +215,6 @@ var View = Backbone.View.extend({ if ( this.event.type ) { e.preventDefault(); this.event.target = this._frameIdentify( e.target ); - if ( this.event.target.grid_lock ) { - this.event.type = null; - return; - } this.event.xy = { x: e.originalEvent.pageX, y: e.originalEvent.pageY @@ -267,22 +284,7 @@ var View = Backbone.View.extend({ _eventFrameClose: function ( e ) { if ( !this.event.type ) { e.preventDefault(); - this.del( this._frameIdentify( e.target ).id ); - } - }, - - /** Lock/Unlock the frame location */ - _eventFrameLock: function ( e ) { - if ( !this.event.type ) { - e.preventDefault(); - var frame = this._frameIdentify( e.target ); - var locked = frame.grid_lock = !frame.grid_lock; - var $el = $( frame.id ); - $el.find( '.f-pin' ) [ locked && 'addClass' || 'removeClass' ]( 'toggle' ); - $el.find( '.f-header' ) [ locked && 'removeClass' || 'addClass' ]( 'f-not-allowed' ); - $el.find( '.f-title' ) [ locked && 'removeClass' || 'addClass' ]( 'f-not-allowed' ); - $el.find( '.f-resize' ) [ locked && 'hide' || 'show' ](); - $el.find( '.f-close' ) [ locked && 'hide' || 'show' ](); + this.del( this._frameIdentify( e.target ) ); } }, @@ -338,7 +340,7 @@ var View = Backbone.View.extend({ this._frameResize( this.frame_shadow, p ); this._frameGrid( this.frame_shadow, frame.grid_location ); frame.grid_location = null; - $( this.frame_shadow.id ).show(); + this.frame_shadow.$el.show(); $( '.f-cover' ).show(); }, @@ -349,7 +351,7 @@ var View = Backbone.View.extend({ this._frameResize( frame, p ); this._frameGrid( frame, this.frame_shadow.grid_location, true ); this.frame_shadow.grid_location = null; - $( this.frame_shadow.id ).hide(); + this.frame_shadow.$el.hide(); $( '.f-cover' ).hide(); this._panelAnimationComplete(); }, @@ -458,6 +460,15 @@ var View = Backbone.View.extend({ FRAME FUNCTIONS */ + /** Initialize a new frame */ + _frameInit: function( frame, id ) { + frame.id = id + frame.screen_location = {}; + frame.grid_location = {}; + frame.grid_rank = null; + frame.$el.attr( 'id', id.substring( 1 ) ); + }, + /** Insert frame at given location */ _frameInsert: function( frame, new_loc, animate ) { var self = this; @@ -467,7 +478,7 @@ var View = Backbone.View.extend({ place_list.push( [ frame, this._locationRank( new_loc ) ] ); } _.each( this.frame_list, function( f ) { - if ( f.grid_location !== null && !f.grid_lock ) { + if ( f.grid_location !== null ) { f.grid_location = null; place_list.push( [ f, f.grid_rank ] ); } @@ -516,7 +527,7 @@ var View = Backbone.View.extend({ /** Handle frame focussing */ _frameFocus: function( frame, has_focus ) { - $( frame.id ).css( 'z-index', this.frame_z + ( has_focus ? 1 : 0 ) ); + frame.$el.css( 'z-index', this.frame_z + ( has_focus ? 1 : 0 ) ); }, /** New left/top position frame */ @@ -526,17 +537,17 @@ var View = Backbone.View.extend({ if ( animate ) { this._frameFocus( frame, true ); var self = this; - $( frame.id ).animate({ top: p.top, left: p.left }, 'fast', function() { + frame.$el.animate({ top: p.top, left: p.left }, 'fast', function() { self._frameFocus( frame, false ); }); } else { - $( frame.id ).css( { top: p.top, left: p.left } ); + frame.$el.css( { top: p.top, left: p.left } ); } }, /** Resize frame */ _frameResize: function( frame, p ) { - $( frame.id ).css( { width: p.width, height: p.height } ); + frame.$el.css( { width: p.width, height: p.height } ); frame.screen_location.width = p.width; frame.screen_location.height = p.height; }, @@ -552,21 +563,6 @@ var View = Backbone.View.extend({ _frameScreen: function( frame ) { var p = frame.screen_location; return { top: p.top, left: p.left, width: p.width, height: p.height }; - }, - - /** Regular frame template */ - _frameTemplate: function( id, title ) { - return '<%= dataset.peek %>',"<% } %>","<% } %>","
Name | Size | Created |
---|
"+JSON.stringify(t.responseJSON)+"":": "+i),creator._showAlert(n,"alert-danger")},render:function(t,e){return this.$el.empty().html(p.templates.main()),this._renderHeader(t),this._renderMiddle(t),this._renderFooter(t),this._addPluginComponents(),this.trigger("rendered",this),this},_renderHeader:function(t,e){var i=this.$(".header").empty().html(p.templates.header()).find(".help-content").prepend(l(p.templates.helpContent()));return this._renderFilters(),i},_renderFilters:function(){return this.$(".forward-column .column-header input").val(this.filters[0]).add(this.$(".reverse-column .column-header input").val(this.filters[1]))},_renderMiddle:function(t,e){var i=this.$(".middle").empty().html(p.templates.middle());return this.unpairedPanelHidden?this.$(".unpaired-columns").hide():this.pairedPanelHidden&&this.$(".paired-columns").hide(),this._renderUnpaired(),this._renderPaired(),i},_renderUnpaired:function(t,e){var i,n,o=this,a=[],r=this._splitByFilters();return this.$(".forward-column .title").text([r[0].length,s("unpaired forward")].join(" ")),this.$(".forward-column .unpaired-info").text(this._renderUnpairedDisplayStr(this.unpaired.length-r[0].length)),this.$(".reverse-column .title").text([r[1].length,s("unpaired reverse")].join(" ")),this.$(".reverse-column .unpaired-info").text(this._renderUnpairedDisplayStr(this.unpaired.length-r[1].length)),this.$(".unpaired-columns .column-datasets").empty(),this.$(".autopair-link").toggle(0!==this.unpaired.length),0===this.unpaired.length?void this._renderUnpairedEmpty():(n=r[1].map(function(t,e){return void 0!==r[0][e]&&r[0][e]!==t&&a.push(o._renderPairButton()),o._renderUnpairedDataset(t)}),i=r[0].map(function(t){return o._renderUnpairedDataset(t)}),i.length||n.length?(this.$(".unpaired-columns .forward-column .column-datasets").append(i).add(this.$(".unpaired-columns .paired-column .column-datasets").append(a)).add(this.$(".unpaired-columns .reverse-column .column-datasets").append(n)),void this._adjUnpairedOnScrollbar()):void this._renderUnpairedNotShown())},_renderUnpairedDisplayStr:function(t){return["(",t," ",s("filtered out"),")"].join("")},_renderUnpairedDataset:function(t){return l("").attr("id","dataset-"+t.id).addClass("dataset unpaired").attr("draggable",!0).addClass(t.selected?"selected":"").append(l("").addClass("dataset-name").text(t.name)).data("dataset",t)},_renderPairButton:function(){return l("").addClass("dataset unpaired").append(l("").addClass("dataset-name").text(s("Pair these datasets")))},_renderUnpairedEmpty:function(){var t=l('').text("("+s("no remaining unpaired datasets")+")");return this.$(".unpaired-columns .paired-column .column-datasets").empty().prepend(t),t},_renderUnpairedNotShown:function(){var t=l('').text("("+s("no datasets were found matching the current filters")+")");return this.$(".unpaired-columns .paired-column .column-datasets").empty().prepend(t),t},_adjUnpairedOnScrollbar:function(){var t=this.$(".unpaired-columns").last(),e=this.$(".unpaired-columns .reverse-column .dataset").first();if(e.length){var i=t.offset().left+t.outerWidth(),n=e.offset().left+e.outerWidth(),s=Math.floor(i)-Math.floor(n);this.$(".unpaired-columns .forward-column").css("margin-left",s>0?s:0)}},_renderPaired:function(t,e){if(this.$(".paired-column-title .title").text([this.paired.length,s("paired")].join(" ")),this.$(".unpair-all-link").toggle(0!==this.paired.length),0===this.paired.length)return void this._renderPairedEmpty();this.$(".remove-extensions-link").show(),this.$(".paired-columns .column-datasets").empty();var i=this;this.paired.forEach(function(t,e){var n=new u({pair:t});i.$(".paired-columns .column-datasets").append(n.render().$el).append(['"].join(""))})},_renderPairedEmpty:function(){var t=l('').text("("+s("no paired datasets yet")+")");return this.$(".paired-columns .column-datasets").empty().prepend(t),t},_renderFooter:function(t,e){var i=this.$(".footer").empty().html(p.templates.footer());return this.$(".remove-extensions").prop("checked",this.removeExtensions),"function"==typeof this.oncancel&&this.$(".cancel-create.btn").show(),i},_addPluginComponents:function(){this._chooseFiltersPopover(".choose-filters-link"),this.$(".help-content i").hoverhighlight(".collection-creator","rgba( 64, 255, 255, 1.0 )")},_chooseFiltersPopover:function(t){function e(t,e){return['"].join("")}var i=l(a.template(['
Name | Size | Created |
---|
"+JSON.stringify(t.responseJSON)+"":": "+i),creator._showAlert(n,"alert-danger")},render:function(t,e){return this.$el.empty().html(p.templates.main()),this._renderHeader(t),this._renderMiddle(t),this._renderFooter(t),this._addPluginComponents(),this.trigger("rendered",this),this},_renderHeader:function(t,e){var i=this.$(".header").empty().html(p.templates.header()).find(".help-content").prepend(l(p.templates.helpContent()));return this._renderFilters(),i},_renderFilters:function(){return this.$(".forward-column .column-header input").val(this.filters[0]).add(this.$(".reverse-column .column-header input").val(this.filters[1]))},_renderMiddle:function(t,e){var i=this.$(".middle").empty().html(p.templates.middle());return this.unpairedPanelHidden?this.$(".unpaired-columns").hide():this.pairedPanelHidden&&this.$(".paired-columns").hide(),this._renderUnpaired(),this._renderPaired(),i},_renderUnpaired:function(t,e){var i,n,o=this,a=[],r=this._splitByFilters();return this.$(".forward-column .title").text([r[0].length,s("unpaired forward")].join(" ")),this.$(".forward-column .unpaired-info").text(this._renderUnpairedDisplayStr(this.unpaired.length-r[0].length)),this.$(".reverse-column .title").text([r[1].length,s("unpaired reverse")].join(" ")),this.$(".reverse-column .unpaired-info").text(this._renderUnpairedDisplayStr(this.unpaired.length-r[1].length)),this.$(".unpaired-columns .column-datasets").empty(),this.$(".autopair-link").toggle(0!==this.unpaired.length),0===this.unpaired.length?void this._renderUnpairedEmpty():(n=r[1].map(function(t,e){return void 0!==r[0][e]&&r[0][e]!==t&&a.push(o._renderPairButton()),o._renderUnpairedDataset(t)}),i=r[0].map(function(t){return o._renderUnpairedDataset(t)}),i.length||n.length?(this.$(".unpaired-columns .forward-column .column-datasets").append(i).add(this.$(".unpaired-columns .paired-column .column-datasets").append(a)).add(this.$(".unpaired-columns .reverse-column .column-datasets").append(n)),void this._adjUnpairedOnScrollbar()):void this._renderUnpairedNotShown())},_renderUnpairedDisplayStr:function(t){return["(",t," ",s("filtered out"),")"].join("")},_renderUnpairedDataset:function(t){return l("").attr("id","dataset-"+t.id).addClass("dataset unpaired").attr("draggable",!0).addClass(t.selected?"selected":"").append(l("").addClass("dataset-name").text(t.name)).data("dataset",t)},_renderPairButton:function(){return l("").addClass("dataset unpaired").append(l("").addClass("dataset-name").text(s("Pair these datasets")))},_renderUnpairedEmpty:function(){var t=l('').text("("+s("no remaining unpaired datasets")+")");return this.$(".unpaired-columns .paired-column .column-datasets").empty().prepend(t),t},_renderUnpairedNotShown:function(){var t=l('').text("("+s("no datasets were found matching the current filters")+")");return this.$(".unpaired-columns .paired-column .column-datasets").empty().prepend(t),t},_adjUnpairedOnScrollbar:function(){var t=this.$(".unpaired-columns").last(),e=this.$(".unpaired-columns .reverse-column .dataset").first();if(e.length){var i=t.offset().left+t.outerWidth(),n=e.offset().left+e.outerWidth(),s=Math.floor(i)-Math.floor(n);this.$(".unpaired-columns .forward-column").css("margin-left",s>0?s:0)}},_renderPaired:function(t,e){if(this.$(".paired-column-title .title").text([this.paired.length,s("paired")].join(" ")),this.$(".unpair-all-link").toggle(0!==this.paired.length),0===this.paired.length)return void this._renderPairedEmpty();this.$(".remove-extensions-link").show(),this.$(".paired-columns .column-datasets").empty();var i=this;this.paired.forEach(function(t,e){var n=new u({pair:t});i.$(".paired-columns .column-datasets").append(n.render().$el).append(['"].join(""))})},_renderPairedEmpty:function(){var t=l('').text("("+s("no paired datasets yet")+")");return this.$(".paired-columns .column-datasets").empty().prepend(t),t},_renderFooter:function(t,e){var i=this.$(".footer").empty().html(p.templates.footer());return this.$(".remove-extensions").prop("checked",this.removeExtensions),"function"==typeof this.oncancel&&this.$(".cancel-create.btn").show(),i},_addPluginComponents:function(){this._chooseFiltersPopover(".choose-filters-link"),this.$(".help-content i").hoverhighlight(".collection-creator","rgba( 64, 255, 255, 1.0 )")},_chooseFiltersPopover:function(t){function e(t,e){return['"].join("")}var i=l(a.template(['
",s(["Collections of paired datasets are ordered lists of dataset pairs (often forward and reverse reads). ","These collections 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, choose which datasets are paired, ","and re-order the final collection."].join("")),"
","",s(['Unpaired datasets are shown in the unpaired section ',"(hover over the underlined words to highlight below). ",'Paired datasets are shown in the paired section.',"
",s(["
",s(["To unpair individual dataset pairs, click the ",'unpair buttons ( ). ','Click the "Unpair all" link to unpair all pairs.'].join("")),"
","",s(['You can include or remove the file extensions (e.g. ".fastq") from your pair names by toggling the ','"Remove file extensions from pair names?" control.'].join("")),"
","",s(['Once your collection is complete, enter a name and ','click "Create list". ',"(Note: you do not have to pair all unpaired datasets to finish.)"].join("")),"
"].join(""))};var g=function(t,e){var i,n=r.Deferred();if(e=a.defaults(e||{},{datasets:t,oncancel:function(){Galaxy.modal.hide(),n.reject("cancelled")},oncreate:function(t,e){Galaxy.modal.hide(),n.resolve(e)}}),!window.Galaxy||!Galaxy.modal)throw new Error("Galaxy or Galaxy.modal not found");return i=new p(e),Galaxy.modal.show({title:"Create a collection of paired datasets",body:i.$el,width:"80%",height:"800px",closing_events:!0}),i.render(),window.creator=i,n};return{PairedCollectionCreator:p,pairedCollectionCreatorModal:g,createListOfPairsCollection:d}}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(3),i(2),i(1),i(1))},function(t,e,i){var n,s;(function(o,a,r){n=[i(31),i(39),i(6),i(5)],s=function(t,e,i,n){"use strict";function s(t){var e=t.toJSON(),i=u(e,{creationFn:function(e,i){return e=[{name:"forward",src:"hda",id:e[0].id},{name:"reverse",src:"hda",id:e[1].id}],t.createHDCA(e,"paired",i)}});return i}var l="collections",c=o.View.extend(i.LoggableMixin).extend({_logNamespace:l,tagName:"li",className:"collection-element",initialize:function(t){this.element=t.element||{},this.identifier=t.identifier},render:function(){return this.$el.attr("data-element-id",this.element.id).html(this.template({identifier:this.identifier,element:this.element})),this},template:a.template(['<%- identifier %>','<%- element.name %>'].join("")),destroy:function(){this.off(),this.$el.remove()},toString:function(){return"DatasetCollectionElementView()"}}),d=t.ListCollectionCreator,h=d.extend({elementViewClass:c,collectionClass:e.HistoryPairDatasetCollection,className:"pair-collection-creator collection-creator flex-row-container",_mangleDuplicateNames:function(){},render:function(t,e){return 2===this.workingElements.length?d.prototype.render.call(this,t,e):this._renderInvalid(t,e)},_renderList:function(t,e){var i=this,n=r(""),s=i.$list();a.each(this.elementViews,function(t){t.destroy(),i.removeElementView(t)}),n.append(i._createForwardElementView().$el),n.append(i._createReverseElementView().$el),s.empty().append(n.children()),a.invoke(i.elementViews,"render")},_createForwardElementView:function(){return this._createElementView(this.workingElements[0],{identifier:"forward"})},_createReverseElementView:function(){return this._createElementView(this.workingElements[1],{identifier:"reverse"})},_createElementView:function(t,e){var i=new this.elementViewClass(a.extend(e,{element:t}));return this.elementViews.push(i),i},swap:function(){this.workingElements=[this.workingElements[1],this.workingElements[0]],this._renderList()},events:a.extend(a.clone(d.prototype.events),{"click .swap":"swap"}),templates:a.extend(a.clone(d.prototype.templates),{middle:a.template(['",n(["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("")),"
","",n(['Once your collection is complete, enter a name and ','click "Create list".'].join("")),"
"].join("")),invalidInitial:a.template([''+this.progressive+"...
";this.modal.$(".modal-body").empty().append(t).css({"margin-top":"8px"})},dialog:function(t,i,n){function s(){var n=t.$("#copy-modal-title").val();if(!n)return void t.$(".invalid-title").show();var s="copy-all"===t.$('input[name="copy-what"]:checked').val();t.$("button").prop("disabled",!0),l._showAjaxIndicator(),i.copy(!0,n,s).done(function(t){c.resolve(t)}).fail(function(){alert([l.errorMessage,e("Please contact a Galaxy administrator")].join(". ")),c.rejectWith(c,arguments)}).always(function(){g&&t.hide()})}n=n||{};var l=this,c=a.Deferred(),d=n.nameFn||this.defaultName,h=d({name:i.get("name")}),u=n.allDatasets?"copy-all":"copy-non-deleted",p=o.isUndefined(n.allowAll)?!0:n.allowAll,g=o.isUndefined(n.autoClose)?!0:n.autoClose;this.modal=t;var f=n.closing_callback;return t.show(o.extend(n,{title:this.title({name:i.get("name")}),body:r(l._template({name:h,isAnon:Galaxy.user.isAnonymous(),allowAll:p,copyWhat:u,activeLabel:this.activeLabel,allLabel:this.allLabel,anonWarning:this.anonWarning})),buttons:o.object([[e("Cancel"),function(){t.hide()}],[this.submitLabel,s]]),height:"auto",closing_events:!0,closing_callback:function(t){t&&c.reject({cancelled:!0}),f&&f(t)}})),t.$("#copy-modal-title").focus().select(),t.$("#copy-modal-title").on("keydown",function(t){13===t.keyCode&&(t.preventDefault(),s())}),c}},n=o.extend({},i,{defaultName:o.template("imported: <%- name %>"),title:o.template(e("Importing history")+' "<%- name %>"'),submitLabel:e("Import"),errorMessage:e("History could not be imported"),progressive:e("Importing history"),activeLabel:e("Import only the active, non-deleted datasets"),allLabel:e("Import all datasets including deleted ones"),anonWarning:e("As an anonymous user, unless you login or register, you will lose your current history ")+e("after importing this history. ")}),s=function(e,s){s=s||{};var o=window.parent.Galaxy.modal||new t.View({});return s.useImport?n.dialog(o,e,s):i.dialog(o,e,s)};return s}.apply(e,n),!(void 0!==s&&(t.exports=s))}).call(e,i(2),i(1),i(1))},function(t,e,i){var n,s;(function(o,a){n=[i(69),i(71),i(6),i(5)],s=function(t,e,i,n){"use strict";var s=t.DatasetListItemEdit,r=s.extend({className:s.prototype.className+" history-content",_fetchModelDetails:function(){var t=this;return t.model.inReadyState()&&!t.model.hasDetails()?t.model.fetch({silent:!0}):t.model.has("rerunnable")?o.when():t.model.fetch({silent:!0,data:{keys:["rerunnable","creating_job"].join(",")}})},events:a.extend(a.clone(s.prototype.events),{"click .unhide-link":function(t){return this.model.unhide(),!1}}),toString:function(){var t=this.model?this.model+"":"(no model)";return"HDAListItemEdit("+t+")"}});return r.prototype.templates=function(){var t=a.extend({},s.prototype.templates.warnings,{hidden:i.wrapTemplate(["<% if( !dataset.visible ){ %>",'Description | Name | Size | Settings | Status |
---|
Name | Size | Type | Genome | Settings | Status |
---|
\" + this.formattedReference() + \"
\" );\n\t return this;\n\t },\n\t formattedReference: function() {\n\t var model = this.model;\n\t var entryType = model.entryType();\n\t var fields = model.fields();\n\t\n\t var ref = \"\";\n\t // Code inspired by...\n\t // https://github.com/vkaravir/bib-publication-list/blob/master/src/bib-publication-list.js\n\t var authorsAndYear = this._asSentence( (fields.author ? fields.author : \"\") + (fields.year ? (\" (\" + fields.year + \")\") : \"\") ) + \" \";\n\t var title = fields.title || \"\";\n\t var pages = fields.pages ? (\"pp. \" + fields.pages) : \"\";\n\t var address = fields.address;\n\t if( entryType == \"article\" ) {\n\t var volume = (fields.volume ? fields.volume : \"\") +\n\t (fields.number ? ( \" (\" + fields.number + \")\" ) : \"\") +\n\t (pages ? \", \" + pages : \"\");\n\t ref = authorsAndYear + this._asSentence(title) +\n\t (fields.journal ? (\"In \" + fields.journal + \", \") : \"\") +\n\t this._asSentence(volume) + \n\t this._asSentence(fields.address) +\n\t \"<\\/em>\";\n\t } else if( entryType == \"inproceedings\" || entryType == \"proceedings\" ) {\n\t ref = authorsAndYear + \n\t this._asSentence(title) + \n\t (fields.booktitle ? (\"In \" + fields.booktitle + \", \") : \"\") +\n\t (pages ? pages : \"\") +\n\t (address ? \", \" + address : \"\") + \n\t \".<\\/em>\";\n\t } else if( entryType == \"mastersthesis\" || entryType == \"phdthesis\" ) {\n\t ref = authorsAndYear + this._asSentence(title) +\n\t (fields.howpublished ? fields.howpublished + \". \" : \"\") +\n\t (fields.note ? fields.note + \".\" : \"\");\n\t } else if( entryType == \"techreport\" ) {\n\t ref = authorsAndYear + this._asSentence(title) +\n\t this._asSentence(fields.institution) +\n\t this._asSentence(fields.number) +\n\t this._asSentence(fields.type);\n\t } else if( entryType == \"book\" || entryType == \"inbook\" || entryType == \"incollection\" ) {\n\t ref = authorsAndYear + \" \" + this._formatBookInfo(fields);\n\t } else {\n\t ref = authorsAndYear + \" \" + this._asSentence(title) +\n\t this._asSentence(fields.howpublished) +\n\t this._asSentence(fields.note);\n\t }\n\t var doiUrl = \"\";\n\t if( fields.doi ) {\n\t doiUrl = 'http://dx.doi.org/' + fields.doi;\n\t ref += '[doi:' + fields.doi + \"]\";\n\t }\n\t var url = fields.url || doiUrl;\n\t if( url ) {\n\t ref += '[Link]';\n\t }\n\t return ref;\n\t },\n\t _formatBookInfo: function(fields) {\n\t var info = \"\";\n\t if( fields.chapter ) {\n\t info += fields.chapter + \" in \";\n\t }\n\t if( fields.title ) {\n\t info += \"\" + fields.title + \"<\\/em>\";\n\t }\n\t if( fields.editor ) {\n\t info += \", Edited by \" + fields.editor + \", \";\n\t }\n\t if( fields.publisher) {\n\t info += \", \" + fields.publisher;\n\t }\n\t if( fields.pages ) {\n\t info += \", pp. \" + fields.pages + \"\";\n\t }\n\t if( fields.series ) {\n\t info += \", \" + fields.series + \"<\\/em>\";\n\t }\n\t if( fields.volume ) {\n\t info += \", Vol.\" + fields.volume;\n\t }\n\t if( fields.issn ) {\n\t info += \", ISBN: \" + fields.issn;\n\t }\n\t return info + \".\";\n\t },\n\t _asSentence: function(str) {\n\t return (str && str.trim()) ? str + \". \" : \"\";\n\t }\n\t});\n\t\n\tvar CitationListView = Backbone.View.extend({\n\t el: '#citations',\n\t /**\n\t * Set up view.\n\t */\n\t initialize: function() {\n\t this.listenTo( this.collection, 'add', this.renderCitation );\n\t },\n\t\n\t events: {\n\t 'click .citations-to-bibtex': 'showBibtex',\n\t 'click .citations-to-formatted': 'showFormatted'\n\t },\n\t\n\t renderCitation: function( citation ) {\n\t var citationView = new CitationView( { model: citation } );\n\t this.$(\".citations-formatted\").append( citationView.render().el );\n\t var rawTextarea = this.$(\".citations-bibtex-text\");\n\t rawTextarea.val( rawTextarea.val() + \"\\n\\r\" + citation.attributes.content );\n\t },\n\t\n\t render: function() {\n\t this.$el.html(this.citationsElement());\n\t this.collection.each(function( item ){\n\t this.renderCitation( item );\n\t }, this);\n\t this.showFormatted();\n\t },\n\t\n\t showBibtex: function() {\n\t this.$(\".citations-to-formatted\").show();\n\t this.$(\".citations-to-bibtex\").hide();\n\t this.$(\".citations-bibtex\").show();\n\t this.$(\".citations-formatted\").hide();\n\t this.$(\".citations-bibtex-text\").select();\n\t },\n\t\n\t showFormatted: function() {\n\t this.$(\".citations-to-formatted\").hide();\n\t this.$(\".citations-to-bibtex\").show();\n\t this.$(\".citations-bibtex\").hide();\n\t this.$(\".citations-formatted\").show();\n\t },\n\t\n\t partialWarningElement: function() {\n\t if( this.collection.partial ) {\n\t return [\n\t '' + JSON.stringify( xhr.responseJSON ) + '';\n\t } else {\n\t content += ': ' + message;\n\t }\n\t }\n\t creator._showAlert( content, 'alert-danger' );\n\t },\n\t\n\t events : {\n\t // header\n\t 'click .more-help' : '_clickMoreHelp',\n\t 'click .less-help' : '_clickLessHelp',\n\t 'click .main-help' : '_toggleHelp',\n\t 'click .header .alert button' : '_hideAlert',\n\t\n\t 'click .reset' : 'reset',\n\t 'click .clear-selected' : 'clearSelectedElements',\n\t\n\t // elements - selection\n\t 'click .collection-elements' : 'clearSelectedElements',\n\t\n\t // elements - drop target\n\t // 'dragenter .collection-elements': '_dragenterElements',\n\t // 'dragleave .collection-elements': '_dragleaveElements',\n\t 'dragover .collection-elements' : '_dragoverElements',\n\t 'drop .collection-elements' : '_dropElements',\n\t\n\t // these bubble up from the elements as custom events\n\t 'collection-element.dragstart .collection-elements' : '_elementDragstart',\n\t 'collection-element.dragend .collection-elements' : '_elementDragend',\n\t\n\t // footer\n\t 'change .collection-name' : '_changeName',\n\t 'keydown .collection-name' : '_nameCheckForEnter',\n\t 'click .cancel-create' : function( ev ){\n\t if( typeof this.oncancel === 'function' ){\n\t this.oncancel.call( this );\n\t }\n\t },\n\t 'click .create-collection' : '_clickCreate'//,\n\t },\n\t\n\t // ........................................................................ header\n\t /** expand help */\n\t _clickMoreHelp : function( ev ){\n\t ev.stopPropagation();\n\t this.$( '.main-help' ).addClass( 'expanded' );\n\t this.$( '.more-help' ).hide();\n\t },\n\t /** collapse help */\n\t _clickLessHelp : function( ev ){\n\t ev.stopPropagation();\n\t this.$( '.main-help' ).removeClass( 'expanded' );\n\t this.$( '.more-help' ).show();\n\t },\n\t /** toggle help */\n\t _toggleHelp : function( ev ){\n\t ev.stopPropagation();\n\t this.$( '.main-help' ).toggleClass( 'expanded' );\n\t this.$( '.more-help' ).toggle();\n\t },\n\t\n\t /** show an alert on the top of the interface containing message (alertClass is bootstrap's alert-*) */\n\t _showAlert : function( message, alertClass ){\n\t alertClass = alertClass || 'alert-danger';\n\t this.$( '.main-help' ).hide();\n\t this.$( '.header .alert' )\n\t .attr( 'class', 'alert alert-dismissable' ).addClass( alertClass ).show()\n\t .find( '.alert-message' ).html( message );\n\t },\n\t /** hide the alerts at the top */\n\t _hideAlert : function( message ){\n\t this.$( '.main-help' ).show();\n\t this.$( '.header .alert' ).hide();\n\t },\n\t\n\t // ........................................................................ elements\n\t /** reset all data to the initial state */\n\t reset : function(){\n\t this._instanceSetUp();\n\t this._elementsSetUp();\n\t this.render();\n\t },\n\t\n\t /** deselect all elements */\n\t clearSelectedElements : function( ev ){\n\t this.$( '.collection-elements .collection-element' ).removeClass( 'selected' );\n\t this.$( '.collection-elements-controls > .clear-selected' ).hide();\n\t },\n\t\n\t //_dragenterElements : function( ev ){\n\t // //this.debug( '_dragenterElements:', ev );\n\t //},\n\t//TODO: if selected are dragged out of the list area - remove the placeholder - cuz it won't work anyway\n\t // _dragleaveElements : function( ev ){\n\t // //this.debug( '_dragleaveElements:', ev );\n\t // },\n\t\n\t /** track the mouse drag over the list adding a placeholder to show where the drop would occur */\n\t _dragoverElements : function( ev ){\n\t //this.debug( '_dragoverElements:', ev );\n\t ev.preventDefault();\n\t\n\t var $list = this.$list();\n\t this._checkForAutoscroll( $list, ev.originalEvent.clientY );\n\t var $nearest = this._getNearestElement( ev.originalEvent.clientY );\n\t\n\t //TODO: no need to re-create - move instead\n\t this.$( '.element-drop-placeholder' ).remove();\n\t var $placeholder = $( '' );\n\t if( !$nearest.length ){\n\t $list.append( $placeholder );\n\t } else {\n\t $nearest.before( $placeholder );\n\t }\n\t },\n\t\n\t /** If the mouse is near enough to the list's top or bottom, scroll the list */\n\t _checkForAutoscroll : function( $element, y ){\n\t var AUTOSCROLL_SPEED = 2,\n\t offset = $element.offset(),\n\t scrollTop = $element.scrollTop(),\n\t upperDist = y - offset.top,\n\t lowerDist = ( offset.top + $element.outerHeight() ) - y;\n\t if( upperDist >= 0 && upperDist < this.autoscrollDist ){\n\t $element.scrollTop( scrollTop - AUTOSCROLL_SPEED );\n\t } else if( lowerDist >= 0 && lowerDist < this.autoscrollDist ){\n\t $element.scrollTop( scrollTop + AUTOSCROLL_SPEED );\n\t }\n\t },\n\t\n\t /** get the nearest element based on the mouse's Y coordinate.\n\t * If the y is at the end of the list, return an empty jQuery object.\n\t */\n\t _getNearestElement : function( y ){\n\t var WIGGLE = 4,\n\t lis = this.$( '.collection-elements li.collection-element' ).toArray();\n\t for( var i=0; i
', _l([\n\t 'Collections of datasets are permanent, ordered lists of datasets that can be passed to tools and ',\n\t 'workflows in order to have analyses done on each member of the entire group. This interface allows ',\n\t 'you to create a collection and re-order the final collection.'\n\t ].join( '' )), '
',\n\t '', _l([\n\t 'Once your collection is complete, enter a name and ',\n\t 'click \"Create list\".'\n\t ].join( '' )), '
'\n\t ].join('')),\n\t\n\t /** shown in list when all elements are discarded */\n\t invalidElements : _.template([\n\t _l( 'The following selections could not be included due to problems:' ),\n\t '<%= dataset.peek %>',\n\t '<% } %>',\n\t '<% } %>',\n\t '
' +\n\t ' | Name | ' +\n\t 'Size | ' +\n\t 'Created | ' +\n\t '
---|
' + JSON.stringify( xhr.responseJSON ) + '';\n\t } else {\n\t content += ': ' + message;\n\t }\n\t }\n\t creator._showAlert( content, 'alert-danger' );\n\t },\n\t\n\t // ------------------------------------------------------------------------ rendering\n\t /** render the entire interface */\n\t render : function( speed, callback ){\n\t //this.debug( '-- _render' );\n\t //this.$el.empty().html( PairedCollectionCreator.templates.main() );\n\t this.$el.empty().html( PairedCollectionCreator.templates.main() );\n\t this._renderHeader( speed );\n\t this._renderMiddle( speed );\n\t this._renderFooter( speed );\n\t this._addPluginComponents();\n\t this.trigger( 'rendered', this );\n\t return this;\n\t },\n\t\n\t /** render the header section */\n\t _renderHeader : function( speed, callback ){\n\t //this.debug( '-- _renderHeader' );\n\t var $header = this.$( '.header' ).empty().html( PairedCollectionCreator.templates.header() )\n\t .find( '.help-content' ).prepend( $( PairedCollectionCreator.templates.helpContent() ) );\n\t\n\t this._renderFilters();\n\t return $header;\n\t },\n\t /** fill the filter inputs with the filter values */\n\t _renderFilters : function(){\n\t return this.$( '.forward-column .column-header input' ).val( this.filters[0] )\n\t .add( this.$( '.reverse-column .column-header input' ).val( this.filters[1] ) );\n\t },\n\t\n\t /** render the middle including unpaired and paired sections (which may be hidden) */\n\t _renderMiddle : function( speed, callback ){\n\t var $middle = this.$( '.middle' ).empty().html( PairedCollectionCreator.templates.middle() );\n\t\n\t // (re-) hide the un/paired panels based on instance vars\n\t if( this.unpairedPanelHidden ){\n\t this.$( '.unpaired-columns' ).hide();\n\t } else if( this.pairedPanelHidden ){\n\t this.$( '.paired-columns' ).hide();\n\t }\n\t\n\t this._renderUnpaired();\n\t this._renderPaired();\n\t return $middle;\n\t },\n\t /** render the unpaired section, showing datasets accrd. to filters, update the unpaired counts */\n\t _renderUnpaired : function( speed, callback ){\n\t //this.debug( '-- _renderUnpaired' );\n\t var creator = this,\n\t $fwd, $rev, $prd = [],\n\t split = this._splitByFilters();\n\t // update unpaired counts\n\t this.$( '.forward-column .title' )\n\t .text([ split[0].length, _l( 'unpaired forward' ) ].join( ' ' ));\n\t this.$( '.forward-column .unpaired-info' )\n\t .text( this._renderUnpairedDisplayStr( this.unpaired.length - split[0].length ) );\n\t this.$( '.reverse-column .title' )\n\t .text([ split[1].length, _l( 'unpaired reverse' ) ].join( ' ' ));\n\t this.$( '.reverse-column .unpaired-info' )\n\t .text( this._renderUnpairedDisplayStr( this.unpaired.length - split[1].length ) );\n\t\n\t this.$( '.unpaired-columns .column-datasets' ).empty();\n\t\n\t // show/hide the auto pair button if any unpaired are left\n\t this.$( '.autopair-link' ).toggle( this.unpaired.length !== 0 );\n\t if( this.unpaired.length === 0 ){\n\t this._renderUnpairedEmpty();\n\t return;\n\t }\n\t\n\t // create the dataset dom arrays\n\t $rev = split[1].map( function( dataset, i ){\n\t // if there'll be a fwd dataset across the way, add a button to pair the row\n\t if( ( split[0][ i ] !== undefined )\n\t && ( split[0][ i ] !== dataset ) ){\n\t $prd.push( creator._renderPairButton() );\n\t }\n\t return creator._renderUnpairedDataset( dataset );\n\t });\n\t $fwd = split[0].map( function( dataset ){\n\t return creator._renderUnpairedDataset( dataset );\n\t });\n\t\n\t if( !$fwd.length && !$rev.length ){\n\t this._renderUnpairedNotShown();\n\t return;\n\t }\n\t // add to appropo cols\n\t //TODO: not the best way to render - consider rendering the entire unpaired-columns section in a fragment\n\t // and swapping out that\n\t this.$( '.unpaired-columns .forward-column .column-datasets' ).append( $fwd )\n\t .add( this.$( '.unpaired-columns .paired-column .column-datasets' ).append( $prd ) )\n\t .add( this.$( '.unpaired-columns .reverse-column .column-datasets' ).append( $rev ) );\n\t this._adjUnpairedOnScrollbar();\n\t },\n\t /** return a string to display the count of filtered out datasets */\n\t _renderUnpairedDisplayStr : function( numFiltered ){\n\t return [ '(', numFiltered, ' ', _l( 'filtered out' ), ')' ].join('');\n\t },\n\t /** return an unattached jQuery DOM element to represent an unpaired dataset */\n\t _renderUnpairedDataset : function( dataset ){\n\t //TODO: to underscore template\n\t return $( '')\n\t .attr( 'id', 'dataset-' + dataset.id )\n\t .addClass( 'dataset unpaired' )\n\t .attr( 'draggable', true )\n\t .addClass( dataset.selected? 'selected': '' )\n\t .append( $( '' ).addClass( 'dataset-name' ).text( dataset.name ) )\n\t //??\n\t .data( 'dataset', dataset );\n\t },\n\t /** render the button that may go between unpaired datasets, allowing the user to pair a row */\n\t _renderPairButton : function(){\n\t //TODO: *not* a dataset - don't pretend like it is\n\t return $( '').addClass( 'dataset unpaired' )\n\t .append( $( '' ).addClass( 'dataset-name' ).text( _l( 'Pair these datasets' ) ) );\n\t },\n\t /** a message to display when no unpaired left */\n\t _renderUnpairedEmpty : function(){\n\t //this.debug( '-- renderUnpairedEmpty' );\n\t var $msg = $( '' )\n\t .text( '(' + _l( 'no remaining unpaired datasets' ) + ')' );\n\t this.$( '.unpaired-columns .paired-column .column-datasets' ).empty().prepend( $msg );\n\t return $msg;\n\t },\n\t /** a message to display when no unpaired can be shown with the current filters */\n\t _renderUnpairedNotShown : function(){\n\t //this.debug( '-- renderUnpairedEmpty' );\n\t var $msg = $( '' )\n\t .text( '(' + _l( 'no datasets were found matching the current filters' ) + ')' );\n\t this.$( '.unpaired-columns .paired-column .column-datasets' ).empty().prepend( $msg );\n\t return $msg;\n\t },\n\t /** try to detect if the unpaired section has a scrollbar and adjust left column for better centering of all */\n\t _adjUnpairedOnScrollbar : function(){\n\t var $unpairedColumns = this.$( '.unpaired-columns' ).last(),\n\t $firstDataset = this.$( '.unpaired-columns .reverse-column .dataset' ).first();\n\t if( !$firstDataset.length ){ return; }\n\t var ucRight = $unpairedColumns.offset().left + $unpairedColumns.outerWidth(),\n\t dsRight = $firstDataset.offset().left + $firstDataset.outerWidth(),\n\t rightDiff = Math.floor( ucRight ) - Math.floor( dsRight );\n\t //this.debug( 'rightDiff:', ucRight, '-', dsRight, '=', rightDiff );\n\t this.$( '.unpaired-columns .forward-column' )\n\t .css( 'margin-left', ( rightDiff > 0 )? rightDiff: 0 );\n\t },\n\t\n\t /** render the paired section and update counts of paired datasets */\n\t _renderPaired : function( speed, callback ){\n\t //this.debug( '-- _renderPaired' );\n\t this.$( '.paired-column-title .title' ).text([ this.paired.length, _l( 'paired' ) ].join( ' ' ) );\n\t // show/hide the unpair all link\n\t this.$( '.unpair-all-link' ).toggle( this.paired.length !== 0 );\n\t if( this.paired.length === 0 ){\n\t this._renderPairedEmpty();\n\t return;\n\t //TODO: would be best to return here (the $columns)\n\t } else {\n\t // show/hide 'remove extensions link' when any paired and they seem to have extensions\n\t this.$( '.remove-extensions-link' ).show();\n\t }\n\t\n\t this.$( '.paired-columns .column-datasets' ).empty();\n\t var creator = this;\n\t this.paired.forEach( function( pair, i ){\n\t //TODO: cache these?\n\t var pairView = new PairView({ pair: pair });\n\t creator.$( '.paired-columns .column-datasets' )\n\t .append( pairView.render().$el )\n\t .append([\n\t ''\n\t ].join( '' ));\n\t });\n\t },\n\t /** a message to display when none paired */\n\t _renderPairedEmpty : function(){\n\t var $msg = $( '' )\n\t .text( '(' + _l( 'no paired datasets yet' ) + ')' );\n\t this.$( '.paired-columns .column-datasets' ).empty().prepend( $msg );\n\t return $msg;\n\t },\n\t\n\t /** render the footer, completion controls, and cancel controls */\n\t _renderFooter : function( speed, callback ){\n\t var $footer = this.$( '.footer' ).empty().html( PairedCollectionCreator.templates.footer() );\n\t this.$( '.remove-extensions' ).prop( 'checked', this.removeExtensions );\n\t if( typeof this.oncancel === 'function' ){\n\t this.$( '.cancel-create.btn' ).show();\n\t }\n\t return $footer;\n\t },\n\t\n\t /** add any jQuery/bootstrap/custom plugins to elements rendered */\n\t _addPluginComponents : function(){\n\t this._chooseFiltersPopover( '.choose-filters-link' );\n\t this.$( '.help-content i' ).hoverhighlight( '.collection-creator', 'rgba( 64, 255, 255, 1.0 )' );\n\t },\n\t\n\t /** build a filter selection popover allowing selection of common filter pairs */\n\t _chooseFiltersPopover : function( selector ){\n\t function filterChoice( val1, val2 ){\n\t return [\n\t ''\n\t ].join('');\n\t }\n\t var $popoverContent = $( _.template([\n\t '
', _l([\n\t 'Collections of paired datasets are ordered lists of dataset pairs (often forward and reverse reads). ',\n\t 'These collections can be passed to tools and workflows in order to have analyses done on each member of ',\n\t 'the entire group. This interface allows you to create a collection, choose which datasets are paired, ',\n\t 'and re-order the final collection.'\n\t ].join( '' )), '
',\n\t '', _l([\n\t 'Unpaired datasets are shown in the unpaired section ',\n\t '(hover over the underlined words to highlight below). ',\n\t 'Paired datasets are shown in the paired section.',\n\t '
', _l([\n\t '
', _l([\n\t 'To unpair individual dataset pairs, click the ',\n\t 'unpair buttons ( ). ',\n\t 'Click the \"Unpair all\" link to unpair all pairs.'\n\t ].join( '' )), '
',\n\t '', _l([\n\t 'You can include or remove the file extensions (e.g. \".fastq\") from your pair names by toggling the ',\n\t '\"Remove file extensions from pair names?\" control.'\n\t ].join( '' )), '
',\n\t '', _l([\n\t 'Once your collection is complete, enter a name and ',\n\t 'click \"Create list\". ',\n\t '(Note: you do not have to pair all unpaired datasets to finish.)'\n\t ].join( '' )), '
'\n\t ].join(''))\n\t};\n\t\n\t\n\t//=============================================================================\n\t/** a modal version of the paired collection creator */\n\tvar pairedCollectionCreatorModal = function _pairedCollectionCreatorModal( datasets, options ){\n\t\n\t var deferred = jQuery.Deferred(),\n\t creator;\n\t\n\t options = _.defaults( options || {}, {\n\t datasets : datasets,\n\t oncancel : function(){\n\t Galaxy.modal.hide();\n\t deferred.reject( 'cancelled' );\n\t },\n\t oncreate : function( creator, response ){\n\t Galaxy.modal.hide();\n\t deferred.resolve( response );\n\t }\n\t });\n\t\n\t if( !window.Galaxy || !Galaxy.modal ){\n\t throw new Error( 'Galaxy or Galaxy.modal not found' );\n\t }\n\t\n\t creator = new PairedCollectionCreator( options );\n\t Galaxy.modal.show({\n\t title : 'Create a collection of paired datasets',\n\t body : creator.$el,\n\t width : '80%',\n\t height : '800px',\n\t closing_events: true\n\t });\n\t creator.render();\n\t window.creator = creator;\n\t\n\t //TODO: remove modal header\n\t return deferred;\n\t};\n\t\n\t\n\t//=============================================================================\n\tfunction createListOfPairsCollection( collection ){\n\t var elements = collection.toJSON();\n\t//TODO: validate elements\n\t return pairedCollectionCreatorModal( elements, {\n\t historyId : collection.historyId\n\t });\n\t}\n\t\n\t\n\t//=============================================================================\n\t return {\n\t PairedCollectionCreator : PairedCollectionCreator,\n\t pairedCollectionCreatorModal : pairedCollectionCreatorModal,\n\t createListOfPairsCollection : createListOfPairsCollection\n\t };\n\t}.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));\n\t\n\t/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(3), __webpack_require__(2), __webpack_require__(1), __webpack_require__(1)))\n\n/***/ },\n/* 100 */\n/***/ function(module, exports, __webpack_require__) {\n\n\tvar __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(Backbone, _, jQuery) {!(__WEBPACK_AMD_DEFINE_ARRAY__ = [\n\t __webpack_require__(31),\n\t __webpack_require__(39),\n\t __webpack_require__(6),\n\t __webpack_require__(5)\n\t], __WEBPACK_AMD_DEFINE_RESULT__ = function( LIST_CREATOR, HDCA, BASE_MVC, _l ){\n\t\n\t'use strict';\n\t\n\tvar logNamespace = 'collections';\n\t/*==============================================================================\n\tTODO:\n\t the paired creator doesn't really mesh with the list creator as parent\n\t it may be better to make an abstract super class for both\n\t composites may inherit from this (or vis-versa)\n\t PairedDatasetCollectionElementView doesn't make a lot of sense\n\t\n\t==============================================================================*/\n\t/** */\n\tvar PairedDatasetCollectionElementView = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend({\n\t _logNamespace : logNamespace,\n\t\n\t//TODO: use proper class (DatasetDCE or NestedDCDCE (or the union of both))\n\t tagName : 'li',\n\t className : 'collection-element',\n\t\n\t initialize : function( attributes ){\n\t this.element = attributes.element || {};\n\t this.identifier = attributes.identifier;\n\t },\n\t\n\t render : function(){\n\t this.$el\n\t .attr( 'data-element-id', this.element.id )\n\t .html( this.template({ identifier: this.identifier, element: this.element }) );\n\t return this;\n\t },\n\t\n\t //TODO: lots of unused space in the element - possibly load details and display them horiz.\n\t template : _.template([\n\t '<%- identifier %>',\n\t '<%- element.name %>',\n\t ].join('')),\n\t\n\t /** remove the DOM and any listeners */\n\t destroy : function(){\n\t this.off();\n\t this.$el.remove();\n\t },\n\t\n\t /** string rep */\n\t toString : function(){\n\t return 'DatasetCollectionElementView()';\n\t }\n\t});\n\t\n\t\n\t// ============================================================================\n\tvar _super = LIST_CREATOR.ListCollectionCreator;\n\t\n\t/** An interface for building collections.\n\t */\n\tvar PairCollectionCreator = _super.extend({\n\t\n\t /** the class used to display individual elements */\n\t elementViewClass : PairedDatasetCollectionElementView,\n\t /** the class this creator will create and save */\n\t collectionClass : HDCA.HistoryPairDatasetCollection,\n\t className : 'pair-collection-creator collection-creator flex-row-container',\n\t\n\t /** override to no-op */\n\t _mangleDuplicateNames : function(){},\n\t\n\t // TODO: this whole pattern sucks. There needs to be two classes of problem area:\n\t // bad inital choices and\n\t // when the user has painted his/her self into a corner during creation/use-of-the-creator\n\t /** render the entire interface */\n\t render : function( speed, callback ){\n\t if( this.workingElements.length === 2 ){\n\t return _super.prototype.render.call( this, speed, callback );\n\t }\n\t return this._renderInvalid( speed, callback );\n\t },\n\t\n\t // ------------------------------------------------------------------------ rendering elements\n\t /** render forward/reverse */\n\t _renderList : function( speed, callback ){\n\t //this.debug( '-- _renderList' );\n\t //precondition: there are two valid elements in workingElements\n\t var creator = this,\n\t $tmp = jQuery( '' ),\n\t $list = creator.$list();\n\t\n\t // lose the original views, create the new, append all at once, then call their renders\n\t _.each( this.elementViews, function( view ){\n\t view.destroy();\n\t creator.removeElementView( view );\n\t });\n\t $tmp.append( creator._createForwardElementView().$el );\n\t $tmp.append( creator._createReverseElementView().$el );\n\t $list.empty().append( $tmp.children() );\n\t _.invoke( creator.elementViews, 'render' );\n\t },\n\t\n\t /** create the forward element view */\n\t _createForwardElementView : function(){\n\t return this._createElementView( this.workingElements[0], { identifier: 'forward' } );\n\t },\n\t\n\t /** create the forward element view */\n\t _createReverseElementView : function(){\n\t return this._createElementView( this.workingElements[1], { identifier: 'reverse' } );\n\t },\n\t\n\t /** create an element view, cache in elementViews, and return */\n\t _createElementView : function( element, options ){\n\t var elementView = new this.elementViewClass( _.extend( options, {\n\t element : element,\n\t }));\n\t this.elementViews.push( elementView );\n\t return elementView;\n\t },\n\t\n\t /** swap the forward, reverse elements and re-render */\n\t swap : function(){\n\t this.workingElements = [\n\t this.workingElements[1],\n\t this.workingElements[0],\n\t ];\n\t this._renderList();\n\t },\n\t\n\t events : _.extend( _.clone( _super.prototype.events ), {\n\t 'click .swap' : 'swap',\n\t }),\n\t\n\t // ------------------------------------------------------------------------ templates\n\t //TODO: move to require text plugin and load these as text\n\t //TODO: underscore currently unnecc. bc no vars are used\n\t //TODO: better way of localizing text-nodes in long strings\n\t /** underscore template fns attached to class */\n\t templates : _.extend( _.clone( _super.prototype.templates ), {\n\t /** the middle: element list */\n\t middle : _.template([\n\t '', _l([\n\t 'Pair collections are permanent collections containing two datasets: one forward and one reverse. ',\n\t 'Often these are forward and reverse reads. The pair collections can be passed to tools and ',\n\t 'workflows in order to have analyses done on both datasets. This interface allows ',\n\t 'you to create a pair, name it, and swap which is forward and which reverse.'\n\t ].join( '' )), '
',\n\t '', _l([\n\t 'Once your collection is complete, enter a name and ',\n\t 'click \"Create list\".'\n\t ].join( '' )), '
'\n\t ].join('')),\n\t\n\t /** a simplified page communicating what went wrong and why the user needs to reselect something else */\n\t invalidInitial : _.template([\n\t '' + this.progressive + '...
';\n\t this.modal.$( '.modal-body' ).empty().append( indicator ).css({ 'margin-top': '8px' });\n\t },\n\t\n\t // (sorta) public interface - display the modal, render the form, and potentially copy the history\n\t // returns a jQuery.Deferred done->history copied, fail->user cancelled\n\t dialog : function _dialog( modal, history, options ){\n\t options = options || {};\n\t\n\t var dialog = this,\n\t deferred = jQuery.Deferred(),\n\t // TODO: getting a little byzantine here\n\t defaultCopyNameFn = options.nameFn || this.defaultName,\n\t defaultCopyName = defaultCopyNameFn({ name: history.get( 'name' ) }),\n\t // TODO: these two might be simpler as one 3 state option (all,active,no-choice)\n\t defaultCopyWhat = options.allDatasets? 'copy-all' : 'copy-non-deleted',\n\t allowAll = !_.isUndefined( options.allowAll )? options.allowAll : true,\n\t autoClose = !_.isUndefined( options.autoClose )? options.autoClose : true;\n\t\n\t this.modal = modal;\n\t\n\t\n\t // validate the name and copy if good\n\t function checkNameAndCopy(){\n\t var name = modal.$( '#copy-modal-title' ).val();\n\t if( !name ){\n\t modal.$( '.invalid-title' ).show();\n\t return;\n\t }\n\t // get further settings, shut down and indicate the ajax call, then hide and resolve/reject\n\t var copyAllDatasets = modal.$( 'input[name=\"copy-what\"]:checked' ).val() === 'copy-all';\n\t modal.$( 'button' ).prop( 'disabled', true );\n\t dialog._showAjaxIndicator();\n\t history.copy( true, name, copyAllDatasets )\n\t .done( function( response ){\n\t deferred.resolve( response );\n\t })\n\t //TODO: make this unneccessary with pub-sub error or handling via Galaxy\n\t .fail( function(){\n\t alert([ dialog.errorMessage, _l( 'Please contact a Galaxy administrator' ) ].join( '. ' ));\n\t deferred.rejectWith( deferred, arguments );\n\t })\n\t .always( function(){\n\t if( autoClose ){ modal.hide(); }\n\t });\n\t }\n\t\n\t var originalClosingCallback = options.closing_callback;\n\t modal.show( _.extend( options, {\n\t title : this.title({ name: history.get( 'name' ) }),\n\t body : $( dialog._template({\n\t name : defaultCopyName,\n\t isAnon : Galaxy.user.isAnonymous(),\n\t allowAll : allowAll,\n\t copyWhat : defaultCopyWhat,\n\t activeLabel : this.activeLabel,\n\t allLabel : this.allLabel,\n\t anonWarning : this.anonWarning,\n\t })),\n\t buttons : _.object([\n\t [ _l( 'Cancel' ), function(){ modal.hide(); } ],\n\t [ this.submitLabel, checkNameAndCopy ]\n\t ]),\n\t height : 'auto',\n\t closing_events : true,\n\t closing_callback: function _historyCopyClose( cancelled ){\n\t if( cancelled ){\n\t deferred.reject({ cancelled : true });\n\t }\n\t if( originalClosingCallback ){\n\t originalClosingCallback( cancelled );\n\t }\n\t }\n\t }));\n\t\n\t // set the default dataset copy, autofocus the title, and set up for a simple return\n\t modal.$( '#copy-modal-title' ).focus().select();\n\t modal.$( '#copy-modal-title' ).on( 'keydown', function( ev ){\n\t if( ev.keyCode === 13 ){\n\t ev.preventDefault();\n\t checkNameAndCopy();\n\t }\n\t });\n\t\n\t return deferred;\n\t },\n\t};\n\t\n\t//==============================================================================\n\t// maintain the (slight) distinction between copy and import\n\t/**\n\t * Subclass CopyDialog to use the import language.\n\t */\n\tvar ImportDialog = _.extend( {}, CopyDialog, {\n\t defaultName : _.template( \"imported: <%- name %>\" ),\n\t title : _.template( _l( 'Importing history' ) + ' \"<%- name %>\"' ),\n\t submitLabel : _l( 'Import' ),\n\t errorMessage : _l( 'History could not be imported' ),\n\t progressive : _l( 'Importing history' ),\n\t activeLabel : _l( 'Import only the active, non-deleted datasets' ),\n\t allLabel : _l( 'Import all datasets including deleted ones' ),\n\t anonWarning : _l( 'As an anonymous user, unless you login or register, you will lose your current history ' ) +\n\t _l( 'after importing this history. ' ),\n\t\n\t});\n\t\n\t//==============================================================================\n\t/**\n\t * Main interface for both history import and history copy dialogs.\n\t * @param {Backbone.Model} history the history to copy\n\t * @param {Object} options a hash\n\t * @return {jQuery.Deferred} promise that fails on close and succeeds on copy\n\t *\n\t * options:\n\t * (this object is also passed to the modal used to display the dialog and accepts modal options)\n\t * {Function} nameFn if defined, use this to build the default name shown to the user\n\t * (the fn is passed: {name:' +\n\t ' | ' +\n\t ' | Description | ' +\n\t 'Name | ' +\n\t 'Size | ' +\n\t 'Settings | ' +\n\t 'Status | ' +\n\t '
---|
Name | ' +\n\t 'Size | ' +\n\t 'Type | ' +\n\t 'Genome | ' +\n\t 'Settings | ' +\n\t 'Status | ' +\n\t '' +\n\t ' |
---|
\" + this.formattedReference() + \"
\" );\n return this;\n },\n formattedReference: function() {\n var model = this.model;\n var entryType = model.entryType();\n var fields = model.fields();\n\n var ref = \"\";\n // Code inspired by...\n // https://github.com/vkaravir/bib-publication-list/blob/master/src/bib-publication-list.js\n var authorsAndYear = this._asSentence( (fields.author ? fields.author : \"\") + (fields.year ? (\" (\" + fields.year + \")\") : \"\") ) + \" \";\n var title = fields.title || \"\";\n var pages = fields.pages ? (\"pp. \" + fields.pages) : \"\";\n var address = fields.address;\n if( entryType == \"article\" ) {\n var volume = (fields.volume ? fields.volume : \"\") +\n (fields.number ? ( \" (\" + fields.number + \")\" ) : \"\") +\n (pages ? \", \" + pages : \"\");\n ref = authorsAndYear + this._asSentence(title) +\n (fields.journal ? (\"In \" + fields.journal + \", \") : \"\") +\n this._asSentence(volume) + \n this._asSentence(fields.address) +\n \"<\\/em>\";\n } else if( entryType == \"inproceedings\" || entryType == \"proceedings\" ) {\n ref = authorsAndYear + \n this._asSentence(title) + \n (fields.booktitle ? (\"In \" + fields.booktitle + \", \") : \"\") +\n (pages ? pages : \"\") +\n (address ? \", \" + address : \"\") + \n \".<\\/em>\";\n } else if( entryType == \"mastersthesis\" || entryType == \"phdthesis\" ) {\n ref = authorsAndYear + this._asSentence(title) +\n (fields.howpublished ? fields.howpublished + \". \" : \"\") +\n (fields.note ? fields.note + \".\" : \"\");\n } else if( entryType == \"techreport\" ) {\n ref = authorsAndYear + this._asSentence(title) +\n this._asSentence(fields.institution) +\n this._asSentence(fields.number) +\n this._asSentence(fields.type);\n } else if( entryType == \"book\" || entryType == \"inbook\" || entryType == \"incollection\" ) {\n ref = authorsAndYear + \" \" + this._formatBookInfo(fields);\n } else {\n ref = authorsAndYear + \" \" + this._asSentence(title) +\n this._asSentence(fields.howpublished) +\n this._asSentence(fields.note);\n }\n var doiUrl = \"\";\n if( fields.doi ) {\n doiUrl = 'http://dx.doi.org/' + fields.doi;\n ref += '[doi:' + fields.doi + \"]\";\n }\n var url = fields.url || doiUrl;\n if( url ) {\n ref += '[Link]';\n }\n return ref;\n },\n _formatBookInfo: function(fields) {\n var info = \"\";\n if( fields.chapter ) {\n info += fields.chapter + \" in \";\n }\n if( fields.title ) {\n info += \"\" + fields.title + \"<\\/em>\";\n }\n if( fields.editor ) {\n info += \", Edited by \" + fields.editor + \", \";\n }\n if( fields.publisher) {\n info += \", \" + fields.publisher;\n }\n if( fields.pages ) {\n info += \", pp. \" + fields.pages + \"\";\n }\n if( fields.series ) {\n info += \", \" + fields.series + \"<\\/em>\";\n }\n if( fields.volume ) {\n info += \", Vol.\" + fields.volume;\n }\n if( fields.issn ) {\n info += \", ISBN: \" + fields.issn;\n }\n return info + \".\";\n },\n _asSentence: function(str) {\n return (str && str.trim()) ? str + \". \" : \"\";\n }\n});\n\nvar CitationListView = Backbone.View.extend({\n el: '#citations',\n /**\n * Set up view.\n */\n initialize: function() {\n this.listenTo( this.collection, 'add', this.renderCitation );\n },\n\n events: {\n 'click .citations-to-bibtex': 'showBibtex',\n 'click .citations-to-formatted': 'showFormatted'\n },\n\n renderCitation: function( citation ) {\n var citationView = new CitationView( { model: citation } );\n this.$(\".citations-formatted\").append( citationView.render().el );\n var rawTextarea = this.$(\".citations-bibtex-text\");\n rawTextarea.val( rawTextarea.val() + \"\\n\\r\" + citation.attributes.content );\n },\n\n render: function() {\n this.$el.html(this.citationsElement());\n this.collection.each(function( item ){\n this.renderCitation( item );\n }, this);\n this.showFormatted();\n },\n\n showBibtex: function() {\n this.$(\".citations-to-formatted\").show();\n this.$(\".citations-to-bibtex\").hide();\n this.$(\".citations-bibtex\").show();\n this.$(\".citations-formatted\").hide();\n this.$(\".citations-bibtex-text\").select();\n },\n\n showFormatted: function() {\n this.$(\".citations-to-formatted\").hide();\n this.$(\".citations-to-bibtex\").show();\n this.$(\".citations-bibtex\").hide();\n this.$(\".citations-formatted\").show();\n },\n\n partialWarningElement: function() {\n if( this.collection.partial ) {\n return [\n '' + JSON.stringify( xhr.responseJSON ) + '';\n } else {\n content += ': ' + message;\n }\n }\n creator._showAlert( content, 'alert-danger' );\n },\n\n events : {\n // header\n 'click .more-help' : '_clickMoreHelp',\n 'click .less-help' : '_clickLessHelp',\n 'click .main-help' : '_toggleHelp',\n 'click .header .alert button' : '_hideAlert',\n\n 'click .reset' : 'reset',\n 'click .clear-selected' : 'clearSelectedElements',\n\n // elements - selection\n 'click .collection-elements' : 'clearSelectedElements',\n\n // elements - drop target\n // 'dragenter .collection-elements': '_dragenterElements',\n // 'dragleave .collection-elements': '_dragleaveElements',\n 'dragover .collection-elements' : '_dragoverElements',\n 'drop .collection-elements' : '_dropElements',\n\n // these bubble up from the elements as custom events\n 'collection-element.dragstart .collection-elements' : '_elementDragstart',\n 'collection-element.dragend .collection-elements' : '_elementDragend',\n\n // footer\n 'change .collection-name' : '_changeName',\n 'keydown .collection-name' : '_nameCheckForEnter',\n 'click .cancel-create' : function( ev ){\n if( typeof this.oncancel === 'function' ){\n this.oncancel.call( this );\n }\n },\n 'click .create-collection' : '_clickCreate'//,\n },\n\n // ........................................................................ header\n /** expand help */\n _clickMoreHelp : function( ev ){\n ev.stopPropagation();\n this.$( '.main-help' ).addClass( 'expanded' );\n this.$( '.more-help' ).hide();\n },\n /** collapse help */\n _clickLessHelp : function( ev ){\n ev.stopPropagation();\n this.$( '.main-help' ).removeClass( 'expanded' );\n this.$( '.more-help' ).show();\n },\n /** toggle help */\n _toggleHelp : function( ev ){\n ev.stopPropagation();\n this.$( '.main-help' ).toggleClass( 'expanded' );\n this.$( '.more-help' ).toggle();\n },\n\n /** show an alert on the top of the interface containing message (alertClass is bootstrap's alert-*) */\n _showAlert : function( message, alertClass ){\n alertClass = alertClass || 'alert-danger';\n this.$( '.main-help' ).hide();\n this.$( '.header .alert' )\n .attr( 'class', 'alert alert-dismissable' ).addClass( alertClass ).show()\n .find( '.alert-message' ).html( message );\n },\n /** hide the alerts at the top */\n _hideAlert : function( message ){\n this.$( '.main-help' ).show();\n this.$( '.header .alert' ).hide();\n },\n\n // ........................................................................ elements\n /** reset all data to the initial state */\n reset : function(){\n this._instanceSetUp();\n this._elementsSetUp();\n this.render();\n },\n\n /** deselect all elements */\n clearSelectedElements : function( ev ){\n this.$( '.collection-elements .collection-element' ).removeClass( 'selected' );\n this.$( '.collection-elements-controls > .clear-selected' ).hide();\n },\n\n //_dragenterElements : function( ev ){\n // //this.debug( '_dragenterElements:', ev );\n //},\n//TODO: if selected are dragged out of the list area - remove the placeholder - cuz it won't work anyway\n // _dragleaveElements : function( ev ){\n // //this.debug( '_dragleaveElements:', ev );\n // },\n\n /** track the mouse drag over the list adding a placeholder to show where the drop would occur */\n _dragoverElements : function( ev ){\n //this.debug( '_dragoverElements:', ev );\n ev.preventDefault();\n\n var $list = this.$list();\n this._checkForAutoscroll( $list, ev.originalEvent.clientY );\n var $nearest = this._getNearestElement( ev.originalEvent.clientY );\n\n //TODO: no need to re-create - move instead\n this.$( '.element-drop-placeholder' ).remove();\n var $placeholder = $( '' );\n if( !$nearest.length ){\n $list.append( $placeholder );\n } else {\n $nearest.before( $placeholder );\n }\n },\n\n /** If the mouse is near enough to the list's top or bottom, scroll the list */\n _checkForAutoscroll : function( $element, y ){\n var AUTOSCROLL_SPEED = 2,\n offset = $element.offset(),\n scrollTop = $element.scrollTop(),\n upperDist = y - offset.top,\n lowerDist = ( offset.top + $element.outerHeight() ) - y;\n if( upperDist >= 0 && upperDist < this.autoscrollDist ){\n $element.scrollTop( scrollTop - AUTOSCROLL_SPEED );\n } else if( lowerDist >= 0 && lowerDist < this.autoscrollDist ){\n $element.scrollTop( scrollTop + AUTOSCROLL_SPEED );\n }\n },\n\n /** get the nearest element based on the mouse's Y coordinate.\n * If the y is at the end of the list, return an empty jQuery object.\n */\n _getNearestElement : function( y ){\n var WIGGLE = 4,\n lis = this.$( '.collection-elements li.collection-element' ).toArray();\n for( var i=0; i
', _l([\n 'Collections of datasets are permanent, ordered lists of datasets that can be passed to tools and ',\n 'workflows in order to have analyses done on each member of the entire group. This interface allows ',\n 'you to create a collection and re-order the final collection.'\n ].join( '' )), '
',\n '', _l([\n 'Once your collection is complete, enter a name and ',\n 'click \"Create list\".'\n ].join( '' )), '
'\n ].join('')),\n\n /** shown in list when all elements are discarded */\n invalidElements : _.template([\n _l( 'The following selections could not be included due to problems:' ),\n '<%= dataset.peek %>',\n '<% } %>',\n '<% } %>',\n '
' +\n ' | Name | ' +\n 'Size | ' +\n 'Created | ' +\n '
---|
' + JSON.stringify( xhr.responseJSON ) + '';\n } else {\n content += ': ' + message;\n }\n }\n creator._showAlert( content, 'alert-danger' );\n },\n\n // ------------------------------------------------------------------------ rendering\n /** render the entire interface */\n render : function( speed, callback ){\n //this.debug( '-- _render' );\n //this.$el.empty().html( PairedCollectionCreator.templates.main() );\n this.$el.empty().html( PairedCollectionCreator.templates.main() );\n this._renderHeader( speed );\n this._renderMiddle( speed );\n this._renderFooter( speed );\n this._addPluginComponents();\n this.trigger( 'rendered', this );\n return this;\n },\n\n /** render the header section */\n _renderHeader : function( speed, callback ){\n //this.debug( '-- _renderHeader' );\n var $header = this.$( '.header' ).empty().html( PairedCollectionCreator.templates.header() )\n .find( '.help-content' ).prepend( $( PairedCollectionCreator.templates.helpContent() ) );\n\n this._renderFilters();\n return $header;\n },\n /** fill the filter inputs with the filter values */\n _renderFilters : function(){\n return this.$( '.forward-column .column-header input' ).val( this.filters[0] )\n .add( this.$( '.reverse-column .column-header input' ).val( this.filters[1] ) );\n },\n\n /** render the middle including unpaired and paired sections (which may be hidden) */\n _renderMiddle : function( speed, callback ){\n var $middle = this.$( '.middle' ).empty().html( PairedCollectionCreator.templates.middle() );\n\n // (re-) hide the un/paired panels based on instance vars\n if( this.unpairedPanelHidden ){\n this.$( '.unpaired-columns' ).hide();\n } else if( this.pairedPanelHidden ){\n this.$( '.paired-columns' ).hide();\n }\n\n this._renderUnpaired();\n this._renderPaired();\n return $middle;\n },\n /** render the unpaired section, showing datasets accrd. to filters, update the unpaired counts */\n _renderUnpaired : function( speed, callback ){\n //this.debug( '-- _renderUnpaired' );\n var creator = this,\n $fwd, $rev, $prd = [],\n split = this._splitByFilters();\n // update unpaired counts\n this.$( '.forward-column .title' )\n .text([ split[0].length, _l( 'unpaired forward' ) ].join( ' ' ));\n this.$( '.forward-column .unpaired-info' )\n .text( this._renderUnpairedDisplayStr( this.unpaired.length - split[0].length ) );\n this.$( '.reverse-column .title' )\n .text([ split[1].length, _l( 'unpaired reverse' ) ].join( ' ' ));\n this.$( '.reverse-column .unpaired-info' )\n .text( this._renderUnpairedDisplayStr( this.unpaired.length - split[1].length ) );\n\n this.$( '.unpaired-columns .column-datasets' ).empty();\n\n // show/hide the auto pair button if any unpaired are left\n this.$( '.autopair-link' ).toggle( this.unpaired.length !== 0 );\n if( this.unpaired.length === 0 ){\n this._renderUnpairedEmpty();\n return;\n }\n\n // create the dataset dom arrays\n $rev = split[1].map( function( dataset, i ){\n // if there'll be a fwd dataset across the way, add a button to pair the row\n if( ( split[0][ i ] !== undefined )\n && ( split[0][ i ] !== dataset ) ){\n $prd.push( creator._renderPairButton() );\n }\n return creator._renderUnpairedDataset( dataset );\n });\n $fwd = split[0].map( function( dataset ){\n return creator._renderUnpairedDataset( dataset );\n });\n\n if( !$fwd.length && !$rev.length ){\n this._renderUnpairedNotShown();\n return;\n }\n // add to appropo cols\n //TODO: not the best way to render - consider rendering the entire unpaired-columns section in a fragment\n // and swapping out that\n this.$( '.unpaired-columns .forward-column .column-datasets' ).append( $fwd )\n .add( this.$( '.unpaired-columns .paired-column .column-datasets' ).append( $prd ) )\n .add( this.$( '.unpaired-columns .reverse-column .column-datasets' ).append( $rev ) );\n this._adjUnpairedOnScrollbar();\n },\n /** return a string to display the count of filtered out datasets */\n _renderUnpairedDisplayStr : function( numFiltered ){\n return [ '(', numFiltered, ' ', _l( 'filtered out' ), ')' ].join('');\n },\n /** return an unattached jQuery DOM element to represent an unpaired dataset */\n _renderUnpairedDataset : function( dataset ){\n //TODO: to underscore template\n return $( '')\n .attr( 'id', 'dataset-' + dataset.id )\n .addClass( 'dataset unpaired' )\n .attr( 'draggable', true )\n .addClass( dataset.selected? 'selected': '' )\n .append( $( '' ).addClass( 'dataset-name' ).text( dataset.name ) )\n //??\n .data( 'dataset', dataset );\n },\n /** render the button that may go between unpaired datasets, allowing the user to pair a row */\n _renderPairButton : function(){\n //TODO: *not* a dataset - don't pretend like it is\n return $( '').addClass( 'dataset unpaired' )\n .append( $( '' ).addClass( 'dataset-name' ).text( _l( 'Pair these datasets' ) ) );\n },\n /** a message to display when no unpaired left */\n _renderUnpairedEmpty : function(){\n //this.debug( '-- renderUnpairedEmpty' );\n var $msg = $( '' )\n .text( '(' + _l( 'no remaining unpaired datasets' ) + ')' );\n this.$( '.unpaired-columns .paired-column .column-datasets' ).empty().prepend( $msg );\n return $msg;\n },\n /** a message to display when no unpaired can be shown with the current filters */\n _renderUnpairedNotShown : function(){\n //this.debug( '-- renderUnpairedEmpty' );\n var $msg = $( '' )\n .text( '(' + _l( 'no datasets were found matching the current filters' ) + ')' );\n this.$( '.unpaired-columns .paired-column .column-datasets' ).empty().prepend( $msg );\n return $msg;\n },\n /** try to detect if the unpaired section has a scrollbar and adjust left column for better centering of all */\n _adjUnpairedOnScrollbar : function(){\n var $unpairedColumns = this.$( '.unpaired-columns' ).last(),\n $firstDataset = this.$( '.unpaired-columns .reverse-column .dataset' ).first();\n if( !$firstDataset.length ){ return; }\n var ucRight = $unpairedColumns.offset().left + $unpairedColumns.outerWidth(),\n dsRight = $firstDataset.offset().left + $firstDataset.outerWidth(),\n rightDiff = Math.floor( ucRight ) - Math.floor( dsRight );\n //this.debug( 'rightDiff:', ucRight, '-', dsRight, '=', rightDiff );\n this.$( '.unpaired-columns .forward-column' )\n .css( 'margin-left', ( rightDiff > 0 )? rightDiff: 0 );\n },\n\n /** render the paired section and update counts of paired datasets */\n _renderPaired : function( speed, callback ){\n //this.debug( '-- _renderPaired' );\n this.$( '.paired-column-title .title' ).text([ this.paired.length, _l( 'paired' ) ].join( ' ' ) );\n // show/hide the unpair all link\n this.$( '.unpair-all-link' ).toggle( this.paired.length !== 0 );\n if( this.paired.length === 0 ){\n this._renderPairedEmpty();\n return;\n //TODO: would be best to return here (the $columns)\n } else {\n // show/hide 'remove extensions link' when any paired and they seem to have extensions\n this.$( '.remove-extensions-link' ).show();\n }\n\n this.$( '.paired-columns .column-datasets' ).empty();\n var creator = this;\n this.paired.forEach( function( pair, i ){\n //TODO: cache these?\n var pairView = new PairView({ pair: pair });\n creator.$( '.paired-columns .column-datasets' )\n .append( pairView.render().$el )\n .append([\n ''\n ].join( '' ));\n });\n },\n /** a message to display when none paired */\n _renderPairedEmpty : function(){\n var $msg = $( '' )\n .text( '(' + _l( 'no paired datasets yet' ) + ')' );\n this.$( '.paired-columns .column-datasets' ).empty().prepend( $msg );\n return $msg;\n },\n\n /** render the footer, completion controls, and cancel controls */\n _renderFooter : function( speed, callback ){\n var $footer = this.$( '.footer' ).empty().html( PairedCollectionCreator.templates.footer() );\n this.$( '.remove-extensions' ).prop( 'checked', this.removeExtensions );\n if( typeof this.oncancel === 'function' ){\n this.$( '.cancel-create.btn' ).show();\n }\n return $footer;\n },\n\n /** add any jQuery/bootstrap/custom plugins to elements rendered */\n _addPluginComponents : function(){\n this._chooseFiltersPopover( '.choose-filters-link' );\n this.$( '.help-content i' ).hoverhighlight( '.collection-creator', 'rgba( 64, 255, 255, 1.0 )' );\n },\n\n /** build a filter selection popover allowing selection of common filter pairs */\n _chooseFiltersPopover : function( selector ){\n function filterChoice( val1, val2 ){\n return [\n ''\n ].join('');\n }\n var $popoverContent = $( _.template([\n '
', _l([\n 'Collections of paired datasets are ordered lists of dataset pairs (often forward and reverse reads). ',\n 'These collections can be passed to tools and workflows in order to have analyses done on each member of ',\n 'the entire group. This interface allows you to create a collection, choose which datasets are paired, ',\n 'and re-order the final collection.'\n ].join( '' )), '
',\n '', _l([\n 'Unpaired datasets are shown in the unpaired section ',\n '(hover over the underlined words to highlight below). ',\n 'Paired datasets are shown in the paired section.',\n '
', _l([\n '
', _l([\n 'To unpair individual dataset pairs, click the ',\n 'unpair buttons ( ). ',\n 'Click the \"Unpair all\" link to unpair all pairs.'\n ].join( '' )), '
',\n '', _l([\n 'You can include or remove the file extensions (e.g. \".fastq\") from your pair names by toggling the ',\n '\"Remove file extensions from pair names?\" control.'\n ].join( '' )), '
',\n '', _l([\n 'Once your collection is complete, enter a name and ',\n 'click \"Create list\". ',\n '(Note: you do not have to pair all unpaired datasets to finish.)'\n ].join( '' )), '
'\n ].join(''))\n};\n\n\n//=============================================================================\n/** a modal version of the paired collection creator */\nvar pairedCollectionCreatorModal = function _pairedCollectionCreatorModal( datasets, options ){\n\n var deferred = jQuery.Deferred(),\n creator;\n\n options = _.defaults( options || {}, {\n datasets : datasets,\n oncancel : function(){\n Galaxy.modal.hide();\n deferred.reject( 'cancelled' );\n },\n oncreate : function( creator, response ){\n Galaxy.modal.hide();\n deferred.resolve( response );\n }\n });\n\n if( !window.Galaxy || !Galaxy.modal ){\n throw new Error( 'Galaxy or Galaxy.modal not found' );\n }\n\n creator = new PairedCollectionCreator( options );\n Galaxy.modal.show({\n title : 'Create a collection of paired datasets',\n body : creator.$el,\n width : '80%',\n height : '800px',\n closing_events: true\n });\n creator.render();\n window.creator = creator;\n\n //TODO: remove modal header\n return deferred;\n};\n\n\n//=============================================================================\nfunction createListOfPairsCollection( collection ){\n var elements = collection.toJSON();\n//TODO: validate elements\n return pairedCollectionCreatorModal( elements, {\n historyId : collection.historyId\n });\n}\n\n\n//=============================================================================\n return {\n PairedCollectionCreator : PairedCollectionCreator,\n pairedCollectionCreatorModal : pairedCollectionCreatorModal,\n createListOfPairsCollection : createListOfPairsCollection\n };\n});\n\n\n\n/*****************\n ** WEBPACK FOOTER\n ** ./galaxy/scripts/mvc/collection/list-of-pairs-collection-creator.js\n ** module id = 99\n ** module chunks = 3\n **/","define([\n \"mvc/collection/list-collection-creator\",\n \"mvc/history/hdca-model\",\n \"mvc/base-mvc\",\n \"utils/localization\"\n], function( LIST_CREATOR, HDCA, BASE_MVC, _l ){\n\n'use strict';\n\nvar logNamespace = 'collections';\n/*==============================================================================\nTODO:\n the paired creator doesn't really mesh with the list creator as parent\n it may be better to make an abstract super class for both\n composites may inherit from this (or vis-versa)\n PairedDatasetCollectionElementView doesn't make a lot of sense\n\n==============================================================================*/\n/** */\nvar PairedDatasetCollectionElementView = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend({\n _logNamespace : logNamespace,\n\n//TODO: use proper class (DatasetDCE or NestedDCDCE (or the union of both))\n tagName : 'li',\n className : 'collection-element',\n\n initialize : function( attributes ){\n this.element = attributes.element || {};\n this.identifier = attributes.identifier;\n },\n\n render : function(){\n this.$el\n .attr( 'data-element-id', this.element.id )\n .html( this.template({ identifier: this.identifier, element: this.element }) );\n return this;\n },\n\n //TODO: lots of unused space in the element - possibly load details and display them horiz.\n template : _.template([\n '<%- identifier %>',\n '<%- element.name %>',\n ].join('')),\n\n /** remove the DOM and any listeners */\n destroy : function(){\n this.off();\n this.$el.remove();\n },\n\n /** string rep */\n toString : function(){\n return 'DatasetCollectionElementView()';\n }\n});\n\n\n// ============================================================================\nvar _super = LIST_CREATOR.ListCollectionCreator;\n\n/** An interface for building collections.\n */\nvar PairCollectionCreator = _super.extend({\n\n /** the class used to display individual elements */\n elementViewClass : PairedDatasetCollectionElementView,\n /** the class this creator will create and save */\n collectionClass : HDCA.HistoryPairDatasetCollection,\n className : 'pair-collection-creator collection-creator flex-row-container',\n\n /** override to no-op */\n _mangleDuplicateNames : function(){},\n\n // TODO: this whole pattern sucks. There needs to be two classes of problem area:\n // bad inital choices and\n // when the user has painted his/her self into a corner during creation/use-of-the-creator\n /** render the entire interface */\n render : function( speed, callback ){\n if( this.workingElements.length === 2 ){\n return _super.prototype.render.call( this, speed, callback );\n }\n return this._renderInvalid( speed, callback );\n },\n\n // ------------------------------------------------------------------------ rendering elements\n /** render forward/reverse */\n _renderList : function( speed, callback ){\n //this.debug( '-- _renderList' );\n //precondition: there are two valid elements in workingElements\n var creator = this,\n $tmp = jQuery( '' ),\n $list = creator.$list();\n\n // lose the original views, create the new, append all at once, then call their renders\n _.each( this.elementViews, function( view ){\n view.destroy();\n creator.removeElementView( view );\n });\n $tmp.append( creator._createForwardElementView().$el );\n $tmp.append( creator._createReverseElementView().$el );\n $list.empty().append( $tmp.children() );\n _.invoke( creator.elementViews, 'render' );\n },\n\n /** create the forward element view */\n _createForwardElementView : function(){\n return this._createElementView( this.workingElements[0], { identifier: 'forward' } );\n },\n\n /** create the forward element view */\n _createReverseElementView : function(){\n return this._createElementView( this.workingElements[1], { identifier: 'reverse' } );\n },\n\n /** create an element view, cache in elementViews, and return */\n _createElementView : function( element, options ){\n var elementView = new this.elementViewClass( _.extend( options, {\n element : element,\n }));\n this.elementViews.push( elementView );\n return elementView;\n },\n\n /** swap the forward, reverse elements and re-render */\n swap : function(){\n this.workingElements = [\n this.workingElements[1],\n this.workingElements[0],\n ];\n this._renderList();\n },\n\n events : _.extend( _.clone( _super.prototype.events ), {\n 'click .swap' : 'swap',\n }),\n\n // ------------------------------------------------------------------------ templates\n //TODO: move to require text plugin and load these as text\n //TODO: underscore currently unnecc. bc no vars are used\n //TODO: better way of localizing text-nodes in long strings\n /** underscore template fns attached to class */\n templates : _.extend( _.clone( _super.prototype.templates ), {\n /** the middle: element list */\n middle : _.template([\n '', _l([\n 'Pair collections are permanent collections containing two datasets: one forward and one reverse. ',\n 'Often these are forward and reverse reads. The pair collections can be passed to tools and ',\n 'workflows in order to have analyses done on both datasets. This interface allows ',\n 'you to create a pair, name it, and swap which is forward and which reverse.'\n ].join( '' )), '
',\n '', _l([\n 'Once your collection is complete, enter a name and ',\n 'click \"Create list\".'\n ].join( '' )), '
'\n ].join('')),\n\n /** a simplified page communicating what went wrong and why the user needs to reselect something else */\n invalidInitial : _.template([\n '' + this.progressive + '...
';\n this.modal.$( '.modal-body' ).empty().append( indicator ).css({ 'margin-top': '8px' });\n },\n\n // (sorta) public interface - display the modal, render the form, and potentially copy the history\n // returns a jQuery.Deferred done->history copied, fail->user cancelled\n dialog : function _dialog( modal, history, options ){\n options = options || {};\n\n var dialog = this,\n deferred = jQuery.Deferred(),\n // TODO: getting a little byzantine here\n defaultCopyNameFn = options.nameFn || this.defaultName,\n defaultCopyName = defaultCopyNameFn({ name: history.get( 'name' ) }),\n // TODO: these two might be simpler as one 3 state option (all,active,no-choice)\n defaultCopyWhat = options.allDatasets? 'copy-all' : 'copy-non-deleted',\n allowAll = !_.isUndefined( options.allowAll )? options.allowAll : true,\n autoClose = !_.isUndefined( options.autoClose )? options.autoClose : true;\n\n this.modal = modal;\n\n\n // validate the name and copy if good\n function checkNameAndCopy(){\n var name = modal.$( '#copy-modal-title' ).val();\n if( !name ){\n modal.$( '.invalid-title' ).show();\n return;\n }\n // get further settings, shut down and indicate the ajax call, then hide and resolve/reject\n var copyAllDatasets = modal.$( 'input[name=\"copy-what\"]:checked' ).val() === 'copy-all';\n modal.$( 'button' ).prop( 'disabled', true );\n dialog._showAjaxIndicator();\n history.copy( true, name, copyAllDatasets )\n .done( function( response ){\n deferred.resolve( response );\n })\n //TODO: make this unneccessary with pub-sub error or handling via Galaxy\n .fail( function(){\n alert([ dialog.errorMessage, _l( 'Please contact a Galaxy administrator' ) ].join( '. ' ));\n deferred.rejectWith( deferred, arguments );\n })\n .always( function(){\n if( autoClose ){ modal.hide(); }\n });\n }\n\n var originalClosingCallback = options.closing_callback;\n modal.show( _.extend( options, {\n title : this.title({ name: history.get( 'name' ) }),\n body : $( dialog._template({\n name : defaultCopyName,\n isAnon : Galaxy.user.isAnonymous(),\n allowAll : allowAll,\n copyWhat : defaultCopyWhat,\n activeLabel : this.activeLabel,\n allLabel : this.allLabel,\n anonWarning : this.anonWarning,\n })),\n buttons : _.object([\n [ _l( 'Cancel' ), function(){ modal.hide(); } ],\n [ this.submitLabel, checkNameAndCopy ]\n ]),\n height : 'auto',\n closing_events : true,\n closing_callback: function _historyCopyClose( cancelled ){\n if( cancelled ){\n deferred.reject({ cancelled : true });\n }\n if( originalClosingCallback ){\n originalClosingCallback( cancelled );\n }\n }\n }));\n\n // set the default dataset copy, autofocus the title, and set up for a simple return\n modal.$( '#copy-modal-title' ).focus().select();\n modal.$( '#copy-modal-title' ).on( 'keydown', function( ev ){\n if( ev.keyCode === 13 ){\n ev.preventDefault();\n checkNameAndCopy();\n }\n });\n\n return deferred;\n },\n};\n\n//==============================================================================\n// maintain the (slight) distinction between copy and import\n/**\n * Subclass CopyDialog to use the import language.\n */\nvar ImportDialog = _.extend( {}, CopyDialog, {\n defaultName : _.template( \"imported: <%- name %>\" ),\n title : _.template( _l( 'Importing history' ) + ' \"<%- name %>\"' ),\n submitLabel : _l( 'Import' ),\n errorMessage : _l( 'History could not be imported' ),\n progressive : _l( 'Importing history' ),\n activeLabel : _l( 'Import only the active, non-deleted datasets' ),\n allLabel : _l( 'Import all datasets including deleted ones' ),\n anonWarning : _l( 'As an anonymous user, unless you login or register, you will lose your current history ' ) +\n _l( 'after importing this history. ' ),\n\n});\n\n//==============================================================================\n/**\n * Main interface for both history import and history copy dialogs.\n * @param {Backbone.Model} history the history to copy\n * @param {Object} options a hash\n * @return {jQuery.Deferred} promise that fails on close and succeeds on copy\n *\n * options:\n * (this object is also passed to the modal used to display the dialog and accepts modal options)\n * {Function} nameFn if defined, use this to build the default name shown to the user\n * (the fn is passed: {name:' +\n ' | ' +\n ' | Description | ' +\n 'Name | ' +\n 'Size | ' +\n 'Settings | ' +\n 'Status | ' +\n '
---|
Name | ' +\n 'Size | ' +\n 'Type | ' +\n 'Genome | ' +\n 'Settings | ' +\n 'Status | ' +\n '' +\n ' |
---|
\" + this.formattedReference() + \"
\" );\n\t return this;\n\t },\n\t formattedReference: function() {\n\t var model = this.model;\n\t var entryType = model.entryType();\n\t var fields = model.fields();\n\t\n\t var ref = \"\";\n\t // Code inspired by...\n\t // https://github.com/vkaravir/bib-publication-list/blob/master/src/bib-publication-list.js\n\t var authorsAndYear = this._asSentence( (fields.author ? fields.author : \"\") + (fields.year ? (\" (\" + fields.year + \")\") : \"\") ) + \" \";\n\t var title = fields.title || \"\";\n\t var pages = fields.pages ? (\"pp. \" + fields.pages) : \"\";\n\t var address = fields.address;\n\t if( entryType == \"article\" ) {\n\t var volume = (fields.volume ? fields.volume : \"\") +\n\t (fields.number ? ( \" (\" + fields.number + \")\" ) : \"\") +\n\t (pages ? \", \" + pages : \"\");\n\t ref = authorsAndYear + this._asSentence(title) +\n\t (fields.journal ? (\"In \" + fields.journal + \", \") : \"\") +\n\t this._asSentence(volume) + \n\t this._asSentence(fields.address) +\n\t \"<\\/em>\";\n\t } else if( entryType == \"inproceedings\" || entryType == \"proceedings\" ) {\n\t ref = authorsAndYear + \n\t this._asSentence(title) + \n\t (fields.booktitle ? (\"In \" + fields.booktitle + \", \") : \"\") +\n\t (pages ? pages : \"\") +\n\t (address ? \", \" + address : \"\") + \n\t \".<\\/em>\";\n\t } else if( entryType == \"mastersthesis\" || entryType == \"phdthesis\" ) {\n\t ref = authorsAndYear + this._asSentence(title) +\n\t (fields.howpublished ? fields.howpublished + \". \" : \"\") +\n\t (fields.note ? fields.note + \".\" : \"\");\n\t } else if( entryType == \"techreport\" ) {\n\t ref = authorsAndYear + this._asSentence(title) +\n\t this._asSentence(fields.institution) +\n\t this._asSentence(fields.number) +\n\t this._asSentence(fields.type);\n\t } else if( entryType == \"book\" || entryType == \"inbook\" || entryType == \"incollection\" ) {\n\t ref = authorsAndYear + \" \" + this._formatBookInfo(fields);\n\t } else {\n\t ref = authorsAndYear + \" \" + this._asSentence(title) +\n\t this._asSentence(fields.howpublished) +\n\t this._asSentence(fields.note);\n\t }\n\t var doiUrl = \"\";\n\t if( fields.doi ) {\n\t doiUrl = 'http://dx.doi.org/' + fields.doi;\n\t ref += '[doi:' + fields.doi + \"]\";\n\t }\n\t var url = fields.url || doiUrl;\n\t if( url ) {\n\t ref += '[Link]';\n\t }\n\t return ref;\n\t },\n\t _formatBookInfo: function(fields) {\n\t var info = \"\";\n\t if( fields.chapter ) {\n\t info += fields.chapter + \" in \";\n\t }\n\t if( fields.title ) {\n\t info += \"\" + fields.title + \"<\\/em>\";\n\t }\n\t if( fields.editor ) {\n\t info += \", Edited by \" + fields.editor + \", \";\n\t }\n\t if( fields.publisher) {\n\t info += \", \" + fields.publisher;\n\t }\n\t if( fields.pages ) {\n\t info += \", pp. \" + fields.pages + \"\";\n\t }\n\t if( fields.series ) {\n\t info += \", \" + fields.series + \"<\\/em>\";\n\t }\n\t if( fields.volume ) {\n\t info += \", Vol.\" + fields.volume;\n\t }\n\t if( fields.issn ) {\n\t info += \", ISBN: \" + fields.issn;\n\t }\n\t return info + \".\";\n\t },\n\t _asSentence: function(str) {\n\t return (str && str.trim()) ? str + \". \" : \"\";\n\t }\n\t});\n\t\n\tvar CitationListView = Backbone.View.extend({\n\t el: '#citations',\n\t /**\n\t * Set up view.\n\t */\n\t initialize: function() {\n\t this.listenTo( this.collection, 'add', this.renderCitation );\n\t },\n\t\n\t events: {\n\t 'click .citations-to-bibtex': 'showBibtex',\n\t 'click .citations-to-formatted': 'showFormatted'\n\t },\n\t\n\t renderCitation: function( citation ) {\n\t var citationView = new CitationView( { model: citation } );\n\t this.$(\".citations-formatted\").append( citationView.render().el );\n\t var rawTextarea = this.$(\".citations-bibtex-text\");\n\t rawTextarea.val( rawTextarea.val() + \"\\n\\r\" + citation.attributes.content );\n\t },\n\t\n\t render: function() {\n\t this.$el.html(this.citationsElement());\n\t this.collection.each(function( item ){\n\t this.renderCitation( item );\n\t }, this);\n\t this.showFormatted();\n\t },\n\t\n\t showBibtex: function() {\n\t this.$(\".citations-to-formatted\").show();\n\t this.$(\".citations-to-bibtex\").hide();\n\t this.$(\".citations-bibtex\").show();\n\t this.$(\".citations-formatted\").hide();\n\t this.$(\".citations-bibtex-text\").select();\n\t },\n\t\n\t showFormatted: function() {\n\t this.$(\".citations-to-formatted\").hide();\n\t this.$(\".citations-to-bibtex\").show();\n\t this.$(\".citations-bibtex\").hide();\n\t this.$(\".citations-formatted\").show();\n\t },\n\t\n\t partialWarningElement: function() {\n\t if( this.collection.partial ) {\n\t return [\n\t '' + JSON.stringify( xhr.responseJSON ) + '';\n\t } else {\n\t content += ': ' + message;\n\t }\n\t }\n\t creator._showAlert( content, 'alert-danger' );\n\t },\n\t\n\t events : {\n\t // header\n\t 'click .more-help' : '_clickMoreHelp',\n\t 'click .less-help' : '_clickLessHelp',\n\t 'click .main-help' : '_toggleHelp',\n\t 'click .header .alert button' : '_hideAlert',\n\t\n\t 'click .reset' : 'reset',\n\t 'click .clear-selected' : 'clearSelectedElements',\n\t\n\t // elements - selection\n\t 'click .collection-elements' : 'clearSelectedElements',\n\t\n\t // elements - drop target\n\t // 'dragenter .collection-elements': '_dragenterElements',\n\t // 'dragleave .collection-elements': '_dragleaveElements',\n\t 'dragover .collection-elements' : '_dragoverElements',\n\t 'drop .collection-elements' : '_dropElements',\n\t\n\t // these bubble up from the elements as custom events\n\t 'collection-element.dragstart .collection-elements' : '_elementDragstart',\n\t 'collection-element.dragend .collection-elements' : '_elementDragend',\n\t\n\t // footer\n\t 'change .collection-name' : '_changeName',\n\t 'keydown .collection-name' : '_nameCheckForEnter',\n\t 'click .cancel-create' : function( ev ){\n\t if( typeof this.oncancel === 'function' ){\n\t this.oncancel.call( this );\n\t }\n\t },\n\t 'click .create-collection' : '_clickCreate'//,\n\t },\n\t\n\t // ........................................................................ header\n\t /** expand help */\n\t _clickMoreHelp : function( ev ){\n\t ev.stopPropagation();\n\t this.$( '.main-help' ).addClass( 'expanded' );\n\t this.$( '.more-help' ).hide();\n\t },\n\t /** collapse help */\n\t _clickLessHelp : function( ev ){\n\t ev.stopPropagation();\n\t this.$( '.main-help' ).removeClass( 'expanded' );\n\t this.$( '.more-help' ).show();\n\t },\n\t /** toggle help */\n\t _toggleHelp : function( ev ){\n\t ev.stopPropagation();\n\t this.$( '.main-help' ).toggleClass( 'expanded' );\n\t this.$( '.more-help' ).toggle();\n\t },\n\t\n\t /** show an alert on the top of the interface containing message (alertClass is bootstrap's alert-*) */\n\t _showAlert : function( message, alertClass ){\n\t alertClass = alertClass || 'alert-danger';\n\t this.$( '.main-help' ).hide();\n\t this.$( '.header .alert' )\n\t .attr( 'class', 'alert alert-dismissable' ).addClass( alertClass ).show()\n\t .find( '.alert-message' ).html( message );\n\t },\n\t /** hide the alerts at the top */\n\t _hideAlert : function( message ){\n\t this.$( '.main-help' ).show();\n\t this.$( '.header .alert' ).hide();\n\t },\n\t\n\t // ........................................................................ elements\n\t /** reset all data to the initial state */\n\t reset : function(){\n\t this._instanceSetUp();\n\t this._elementsSetUp();\n\t this.render();\n\t },\n\t\n\t /** deselect all elements */\n\t clearSelectedElements : function( ev ){\n\t this.$( '.collection-elements .collection-element' ).removeClass( 'selected' );\n\t this.$( '.collection-elements-controls > .clear-selected' ).hide();\n\t },\n\t\n\t //_dragenterElements : function( ev ){\n\t // //this.debug( '_dragenterElements:', ev );\n\t //},\n\t//TODO: if selected are dragged out of the list area - remove the placeholder - cuz it won't work anyway\n\t // _dragleaveElements : function( ev ){\n\t // //this.debug( '_dragleaveElements:', ev );\n\t // },\n\t\n\t /** track the mouse drag over the list adding a placeholder to show where the drop would occur */\n\t _dragoverElements : function( ev ){\n\t //this.debug( '_dragoverElements:', ev );\n\t ev.preventDefault();\n\t\n\t var $list = this.$list();\n\t this._checkForAutoscroll( $list, ev.originalEvent.clientY );\n\t var $nearest = this._getNearestElement( ev.originalEvent.clientY );\n\t\n\t //TODO: no need to re-create - move instead\n\t this.$( '.element-drop-placeholder' ).remove();\n\t var $placeholder = $( '' );\n\t if( !$nearest.length ){\n\t $list.append( $placeholder );\n\t } else {\n\t $nearest.before( $placeholder );\n\t }\n\t },\n\t\n\t /** If the mouse is near enough to the list's top or bottom, scroll the list */\n\t _checkForAutoscroll : function( $element, y ){\n\t var AUTOSCROLL_SPEED = 2,\n\t offset = $element.offset(),\n\t scrollTop = $element.scrollTop(),\n\t upperDist = y - offset.top,\n\t lowerDist = ( offset.top + $element.outerHeight() ) - y;\n\t if( upperDist >= 0 && upperDist < this.autoscrollDist ){\n\t $element.scrollTop( scrollTop - AUTOSCROLL_SPEED );\n\t } else if( lowerDist >= 0 && lowerDist < this.autoscrollDist ){\n\t $element.scrollTop( scrollTop + AUTOSCROLL_SPEED );\n\t }\n\t },\n\t\n\t /** get the nearest element based on the mouse's Y coordinate.\n\t * If the y is at the end of the list, return an empty jQuery object.\n\t */\n\t _getNearestElement : function( y ){\n\t var WIGGLE = 4,\n\t lis = this.$( '.collection-elements li.collection-element' ).toArray();\n\t for( var i=0; i
', _l([\n\t 'Collections of datasets are permanent, ordered lists of datasets that can be passed to tools and ',\n\t 'workflows in order to have analyses done on each member of the entire group. This interface allows ',\n\t 'you to create a collection and re-order the final collection.'\n\t ].join( '' )), '
',\n\t '', _l([\n\t 'Once your collection is complete, enter a name and ',\n\t 'click \"Create list\".'\n\t ].join( '' )), '
'\n\t ].join('')),\n\t\n\t /** shown in list when all elements are discarded */\n\t invalidElements : _.template([\n\t _l( 'The following selections could not be included due to problems:' ),\n\t '<%= dataset.peek %>',\n\t '<% } %>',\n\t '<% } %>',\n\t '
' +\n\t ' | Name | ' +\n\t 'Size | ' +\n\t 'Created | ' +\n\t '
---|
' + JSON.stringify( xhr.responseJSON ) + '';\n\t } else {\n\t content += ': ' + message;\n\t }\n\t }\n\t creator._showAlert( content, 'alert-danger' );\n\t },\n\t\n\t // ------------------------------------------------------------------------ rendering\n\t /** render the entire interface */\n\t render : function( speed, callback ){\n\t //this.debug( '-- _render' );\n\t //this.$el.empty().html( PairedCollectionCreator.templates.main() );\n\t this.$el.empty().html( PairedCollectionCreator.templates.main() );\n\t this._renderHeader( speed );\n\t this._renderMiddle( speed );\n\t this._renderFooter( speed );\n\t this._addPluginComponents();\n\t this.trigger( 'rendered', this );\n\t return this;\n\t },\n\t\n\t /** render the header section */\n\t _renderHeader : function( speed, callback ){\n\t //this.debug( '-- _renderHeader' );\n\t var $header = this.$( '.header' ).empty().html( PairedCollectionCreator.templates.header() )\n\t .find( '.help-content' ).prepend( $( PairedCollectionCreator.templates.helpContent() ) );\n\t\n\t this._renderFilters();\n\t return $header;\n\t },\n\t /** fill the filter inputs with the filter values */\n\t _renderFilters : function(){\n\t return this.$( '.forward-column .column-header input' ).val( this.filters[0] )\n\t .add( this.$( '.reverse-column .column-header input' ).val( this.filters[1] ) );\n\t },\n\t\n\t /** render the middle including unpaired and paired sections (which may be hidden) */\n\t _renderMiddle : function( speed, callback ){\n\t var $middle = this.$( '.middle' ).empty().html( PairedCollectionCreator.templates.middle() );\n\t\n\t // (re-) hide the un/paired panels based on instance vars\n\t if( this.unpairedPanelHidden ){\n\t this.$( '.unpaired-columns' ).hide();\n\t } else if( this.pairedPanelHidden ){\n\t this.$( '.paired-columns' ).hide();\n\t }\n\t\n\t this._renderUnpaired();\n\t this._renderPaired();\n\t return $middle;\n\t },\n\t /** render the unpaired section, showing datasets accrd. to filters, update the unpaired counts */\n\t _renderUnpaired : function( speed, callback ){\n\t //this.debug( '-- _renderUnpaired' );\n\t var creator = this,\n\t $fwd, $rev, $prd = [],\n\t split = this._splitByFilters();\n\t // update unpaired counts\n\t this.$( '.forward-column .title' )\n\t .text([ split[0].length, _l( 'unpaired forward' ) ].join( ' ' ));\n\t this.$( '.forward-column .unpaired-info' )\n\t .text( this._renderUnpairedDisplayStr( this.unpaired.length - split[0].length ) );\n\t this.$( '.reverse-column .title' )\n\t .text([ split[1].length, _l( 'unpaired reverse' ) ].join( ' ' ));\n\t this.$( '.reverse-column .unpaired-info' )\n\t .text( this._renderUnpairedDisplayStr( this.unpaired.length - split[1].length ) );\n\t\n\t this.$( '.unpaired-columns .column-datasets' ).empty();\n\t\n\t // show/hide the auto pair button if any unpaired are left\n\t this.$( '.autopair-link' ).toggle( this.unpaired.length !== 0 );\n\t if( this.unpaired.length === 0 ){\n\t this._renderUnpairedEmpty();\n\t return;\n\t }\n\t\n\t // create the dataset dom arrays\n\t $rev = split[1].map( function( dataset, i ){\n\t // if there'll be a fwd dataset across the way, add a button to pair the row\n\t if( ( split[0][ i ] !== undefined )\n\t && ( split[0][ i ] !== dataset ) ){\n\t $prd.push( creator._renderPairButton() );\n\t }\n\t return creator._renderUnpairedDataset( dataset );\n\t });\n\t $fwd = split[0].map( function( dataset ){\n\t return creator._renderUnpairedDataset( dataset );\n\t });\n\t\n\t if( !$fwd.length && !$rev.length ){\n\t this._renderUnpairedNotShown();\n\t return;\n\t }\n\t // add to appropo cols\n\t //TODO: not the best way to render - consider rendering the entire unpaired-columns section in a fragment\n\t // and swapping out that\n\t this.$( '.unpaired-columns .forward-column .column-datasets' ).append( $fwd )\n\t .add( this.$( '.unpaired-columns .paired-column .column-datasets' ).append( $prd ) )\n\t .add( this.$( '.unpaired-columns .reverse-column .column-datasets' ).append( $rev ) );\n\t this._adjUnpairedOnScrollbar();\n\t },\n\t /** return a string to display the count of filtered out datasets */\n\t _renderUnpairedDisplayStr : function( numFiltered ){\n\t return [ '(', numFiltered, ' ', _l( 'filtered out' ), ')' ].join('');\n\t },\n\t /** return an unattached jQuery DOM element to represent an unpaired dataset */\n\t _renderUnpairedDataset : function( dataset ){\n\t //TODO: to underscore template\n\t return $( '')\n\t .attr( 'id', 'dataset-' + dataset.id )\n\t .addClass( 'dataset unpaired' )\n\t .attr( 'draggable', true )\n\t .addClass( dataset.selected? 'selected': '' )\n\t .append( $( '' ).addClass( 'dataset-name' ).text( dataset.name ) )\n\t //??\n\t .data( 'dataset', dataset );\n\t },\n\t /** render the button that may go between unpaired datasets, allowing the user to pair a row */\n\t _renderPairButton : function(){\n\t //TODO: *not* a dataset - don't pretend like it is\n\t return $( '').addClass( 'dataset unpaired' )\n\t .append( $( '' ).addClass( 'dataset-name' ).text( _l( 'Pair these datasets' ) ) );\n\t },\n\t /** a message to display when no unpaired left */\n\t _renderUnpairedEmpty : function(){\n\t //this.debug( '-- renderUnpairedEmpty' );\n\t var $msg = $( '' )\n\t .text( '(' + _l( 'no remaining unpaired datasets' ) + ')' );\n\t this.$( '.unpaired-columns .paired-column .column-datasets' ).empty().prepend( $msg );\n\t return $msg;\n\t },\n\t /** a message to display when no unpaired can be shown with the current filters */\n\t _renderUnpairedNotShown : function(){\n\t //this.debug( '-- renderUnpairedEmpty' );\n\t var $msg = $( '' )\n\t .text( '(' + _l( 'no datasets were found matching the current filters' ) + ')' );\n\t this.$( '.unpaired-columns .paired-column .column-datasets' ).empty().prepend( $msg );\n\t return $msg;\n\t },\n\t /** try to detect if the unpaired section has a scrollbar and adjust left column for better centering of all */\n\t _adjUnpairedOnScrollbar : function(){\n\t var $unpairedColumns = this.$( '.unpaired-columns' ).last(),\n\t $firstDataset = this.$( '.unpaired-columns .reverse-column .dataset' ).first();\n\t if( !$firstDataset.length ){ return; }\n\t var ucRight = $unpairedColumns.offset().left + $unpairedColumns.outerWidth(),\n\t dsRight = $firstDataset.offset().left + $firstDataset.outerWidth(),\n\t rightDiff = Math.floor( ucRight ) - Math.floor( dsRight );\n\t //this.debug( 'rightDiff:', ucRight, '-', dsRight, '=', rightDiff );\n\t this.$( '.unpaired-columns .forward-column' )\n\t .css( 'margin-left', ( rightDiff > 0 )? rightDiff: 0 );\n\t },\n\t\n\t /** render the paired section and update counts of paired datasets */\n\t _renderPaired : function( speed, callback ){\n\t //this.debug( '-- _renderPaired' );\n\t this.$( '.paired-column-title .title' ).text([ this.paired.length, _l( 'paired' ) ].join( ' ' ) );\n\t // show/hide the unpair all link\n\t this.$( '.unpair-all-link' ).toggle( this.paired.length !== 0 );\n\t if( this.paired.length === 0 ){\n\t this._renderPairedEmpty();\n\t return;\n\t //TODO: would be best to return here (the $columns)\n\t } else {\n\t // show/hide 'remove extensions link' when any paired and they seem to have extensions\n\t this.$( '.remove-extensions-link' ).show();\n\t }\n\t\n\t this.$( '.paired-columns .column-datasets' ).empty();\n\t var creator = this;\n\t this.paired.forEach( function( pair, i ){\n\t //TODO: cache these?\n\t var pairView = new PairView({ pair: pair });\n\t creator.$( '.paired-columns .column-datasets' )\n\t .append( pairView.render().$el )\n\t .append([\n\t ''\n\t ].join( '' ));\n\t });\n\t },\n\t /** a message to display when none paired */\n\t _renderPairedEmpty : function(){\n\t var $msg = $( '' )\n\t .text( '(' + _l( 'no paired datasets yet' ) + ')' );\n\t this.$( '.paired-columns .column-datasets' ).empty().prepend( $msg );\n\t return $msg;\n\t },\n\t\n\t /** render the footer, completion controls, and cancel controls */\n\t _renderFooter : function( speed, callback ){\n\t var $footer = this.$( '.footer' ).empty().html( PairedCollectionCreator.templates.footer() );\n\t this.$( '.remove-extensions' ).prop( 'checked', this.removeExtensions );\n\t if( typeof this.oncancel === 'function' ){\n\t this.$( '.cancel-create.btn' ).show();\n\t }\n\t return $footer;\n\t },\n\t\n\t /** add any jQuery/bootstrap/custom plugins to elements rendered */\n\t _addPluginComponents : function(){\n\t this._chooseFiltersPopover( '.choose-filters-link' );\n\t this.$( '.help-content i' ).hoverhighlight( '.collection-creator', 'rgba( 64, 255, 255, 1.0 )' );\n\t },\n\t\n\t /** build a filter selection popover allowing selection of common filter pairs */\n\t _chooseFiltersPopover : function( selector ){\n\t function filterChoice( val1, val2 ){\n\t return [\n\t ''\n\t ].join('');\n\t }\n\t var $popoverContent = $( _.template([\n\t '
', _l([\n\t 'Collections of paired datasets are ordered lists of dataset pairs (often forward and reverse reads). ',\n\t 'These collections can be passed to tools and workflows in order to have analyses done on each member of ',\n\t 'the entire group. This interface allows you to create a collection, choose which datasets are paired, ',\n\t 'and re-order the final collection.'\n\t ].join( '' )), '
',\n\t '', _l([\n\t 'Unpaired datasets are shown in the unpaired section ',\n\t '(hover over the underlined words to highlight below). ',\n\t 'Paired datasets are shown in the paired section.',\n\t '
', _l([\n\t '
', _l([\n\t 'To unpair individual dataset pairs, click the ',\n\t 'unpair buttons ( ). ',\n\t 'Click the \"Unpair all\" link to unpair all pairs.'\n\t ].join( '' )), '
',\n\t '', _l([\n\t 'You can include or remove the file extensions (e.g. \".fastq\") from your pair names by toggling the ',\n\t '\"Remove file extensions from pair names?\" control.'\n\t ].join( '' )), '
',\n\t '', _l([\n\t 'Once your collection is complete, enter a name and ',\n\t 'click \"Create list\". ',\n\t '(Note: you do not have to pair all unpaired datasets to finish.)'\n\t ].join( '' )), '
'\n\t ].join(''))\n\t};\n\t\n\t\n\t//=============================================================================\n\t/** a modal version of the paired collection creator */\n\tvar pairedCollectionCreatorModal = function _pairedCollectionCreatorModal( datasets, options ){\n\t\n\t var deferred = jQuery.Deferred(),\n\t creator;\n\t\n\t options = _.defaults( options || {}, {\n\t datasets : datasets,\n\t oncancel : function(){\n\t Galaxy.modal.hide();\n\t deferred.reject( 'cancelled' );\n\t },\n\t oncreate : function( creator, response ){\n\t Galaxy.modal.hide();\n\t deferred.resolve( response );\n\t }\n\t });\n\t\n\t if( !window.Galaxy || !Galaxy.modal ){\n\t throw new Error( 'Galaxy or Galaxy.modal not found' );\n\t }\n\t\n\t creator = new PairedCollectionCreator( options );\n\t Galaxy.modal.show({\n\t title : 'Create a collection of paired datasets',\n\t body : creator.$el,\n\t width : '80%',\n\t height : '800px',\n\t closing_events: true\n\t });\n\t creator.render();\n\t window.creator = creator;\n\t\n\t //TODO: remove modal header\n\t return deferred;\n\t};\n\t\n\t\n\t//=============================================================================\n\tfunction createListOfPairsCollection( collection ){\n\t var elements = collection.toJSON();\n\t//TODO: validate elements\n\t return pairedCollectionCreatorModal( elements, {\n\t historyId : collection.historyId\n\t });\n\t}\n\t\n\t\n\t//=============================================================================\n\t return {\n\t PairedCollectionCreator : PairedCollectionCreator,\n\t pairedCollectionCreatorModal : pairedCollectionCreatorModal,\n\t createListOfPairsCollection : createListOfPairsCollection\n\t };\n\t}.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));\n\t\n\t/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(3), __webpack_require__(2), __webpack_require__(1), __webpack_require__(1)))\n\n/***/ },\n/* 100 */\n/***/ function(module, exports, __webpack_require__) {\n\n\tvar __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/* WEBPACK VAR INJECTION */(function(Backbone, _, jQuery) {!(__WEBPACK_AMD_DEFINE_ARRAY__ = [\n\t __webpack_require__(31),\n\t __webpack_require__(39),\n\t __webpack_require__(6),\n\t __webpack_require__(5)\n\t], __WEBPACK_AMD_DEFINE_RESULT__ = function( LIST_CREATOR, HDCA, BASE_MVC, _l ){\n\t\n\t'use strict';\n\t\n\tvar logNamespace = 'collections';\n\t/*==============================================================================\n\tTODO:\n\t the paired creator doesn't really mesh with the list creator as parent\n\t it may be better to make an abstract super class for both\n\t composites may inherit from this (or vis-versa)\n\t PairedDatasetCollectionElementView doesn't make a lot of sense\n\t\n\t==============================================================================*/\n\t/** */\n\tvar PairedDatasetCollectionElementView = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend({\n\t _logNamespace : logNamespace,\n\t\n\t//TODO: use proper class (DatasetDCE or NestedDCDCE (or the union of both))\n\t tagName : 'li',\n\t className : 'collection-element',\n\t\n\t initialize : function( attributes ){\n\t this.element = attributes.element || {};\n\t this.identifier = attributes.identifier;\n\t },\n\t\n\t render : function(){\n\t this.$el\n\t .attr( 'data-element-id', this.element.id )\n\t .html( this.template({ identifier: this.identifier, element: this.element }) );\n\t return this;\n\t },\n\t\n\t //TODO: lots of unused space in the element - possibly load details and display them horiz.\n\t template : _.template([\n\t '<%- identifier %>',\n\t '<%- element.name %>',\n\t ].join('')),\n\t\n\t /** remove the DOM and any listeners */\n\t destroy : function(){\n\t this.off();\n\t this.$el.remove();\n\t },\n\t\n\t /** string rep */\n\t toString : function(){\n\t return 'DatasetCollectionElementView()';\n\t }\n\t});\n\t\n\t\n\t// ============================================================================\n\tvar _super = LIST_CREATOR.ListCollectionCreator;\n\t\n\t/** An interface for building collections.\n\t */\n\tvar PairCollectionCreator = _super.extend({\n\t\n\t /** the class used to display individual elements */\n\t elementViewClass : PairedDatasetCollectionElementView,\n\t /** the class this creator will create and save */\n\t collectionClass : HDCA.HistoryPairDatasetCollection,\n\t className : 'pair-collection-creator collection-creator flex-row-container',\n\t\n\t /** override to no-op */\n\t _mangleDuplicateNames : function(){},\n\t\n\t // TODO: this whole pattern sucks. There needs to be two classes of problem area:\n\t // bad inital choices and\n\t // when the user has painted his/her self into a corner during creation/use-of-the-creator\n\t /** render the entire interface */\n\t render : function( speed, callback ){\n\t if( this.workingElements.length === 2 ){\n\t return _super.prototype.render.call( this, speed, callback );\n\t }\n\t return this._renderInvalid( speed, callback );\n\t },\n\t\n\t // ------------------------------------------------------------------------ rendering elements\n\t /** render forward/reverse */\n\t _renderList : function( speed, callback ){\n\t //this.debug( '-- _renderList' );\n\t //precondition: there are two valid elements in workingElements\n\t var creator = this,\n\t $tmp = jQuery( '' ),\n\t $list = creator.$list();\n\t\n\t // lose the original views, create the new, append all at once, then call their renders\n\t _.each( this.elementViews, function( view ){\n\t view.destroy();\n\t creator.removeElementView( view );\n\t });\n\t $tmp.append( creator._createForwardElementView().$el );\n\t $tmp.append( creator._createReverseElementView().$el );\n\t $list.empty().append( $tmp.children() );\n\t _.invoke( creator.elementViews, 'render' );\n\t },\n\t\n\t /** create the forward element view */\n\t _createForwardElementView : function(){\n\t return this._createElementView( this.workingElements[0], { identifier: 'forward' } );\n\t },\n\t\n\t /** create the forward element view */\n\t _createReverseElementView : function(){\n\t return this._createElementView( this.workingElements[1], { identifier: 'reverse' } );\n\t },\n\t\n\t /** create an element view, cache in elementViews, and return */\n\t _createElementView : function( element, options ){\n\t var elementView = new this.elementViewClass( _.extend( options, {\n\t element : element,\n\t }));\n\t this.elementViews.push( elementView );\n\t return elementView;\n\t },\n\t\n\t /** swap the forward, reverse elements and re-render */\n\t swap : function(){\n\t this.workingElements = [\n\t this.workingElements[1],\n\t this.workingElements[0],\n\t ];\n\t this._renderList();\n\t },\n\t\n\t events : _.extend( _.clone( _super.prototype.events ), {\n\t 'click .swap' : 'swap',\n\t }),\n\t\n\t // ------------------------------------------------------------------------ templates\n\t //TODO: move to require text plugin and load these as text\n\t //TODO: underscore currently unnecc. bc no vars are used\n\t //TODO: better way of localizing text-nodes in long strings\n\t /** underscore template fns attached to class */\n\t templates : _.extend( _.clone( _super.prototype.templates ), {\n\t /** the middle: element list */\n\t middle : _.template([\n\t '', _l([\n\t 'Pair collections are permanent collections containing two datasets: one forward and one reverse. ',\n\t 'Often these are forward and reverse reads. The pair collections can be passed to tools and ',\n\t 'workflows in order to have analyses done on both datasets. This interface allows ',\n\t 'you to create a pair, name it, and swap which is forward and which reverse.'\n\t ].join( '' )), '
',\n\t '', _l([\n\t 'Once your collection is complete, enter a name and ',\n\t 'click \"Create list\".'\n\t ].join( '' )), '
'\n\t ].join('')),\n\t\n\t /** a simplified page communicating what went wrong and why the user needs to reselect something else */\n\t invalidInitial : _.template([\n\t '' + this.progressive + '...
';\n\t this.modal.$( '.modal-body' ).empty().append( indicator ).css({ 'margin-top': '8px' });\n\t },\n\t\n\t // (sorta) public interface - display the modal, render the form, and potentially copy the history\n\t // returns a jQuery.Deferred done->history copied, fail->user cancelled\n\t dialog : function _dialog( modal, history, options ){\n\t options = options || {};\n\t\n\t var dialog = this,\n\t deferred = jQuery.Deferred(),\n\t // TODO: getting a little byzantine here\n\t defaultCopyNameFn = options.nameFn || this.defaultName,\n\t defaultCopyName = defaultCopyNameFn({ name: history.get( 'name' ) }),\n\t // TODO: these two might be simpler as one 3 state option (all,active,no-choice)\n\t defaultCopyWhat = options.allDatasets? 'copy-all' : 'copy-non-deleted',\n\t allowAll = !_.isUndefined( options.allowAll )? options.allowAll : true,\n\t autoClose = !_.isUndefined( options.autoClose )? options.autoClose : true;\n\t\n\t this.modal = modal;\n\t\n\t\n\t // validate the name and copy if good\n\t function checkNameAndCopy(){\n\t var name = modal.$( '#copy-modal-title' ).val();\n\t if( !name ){\n\t modal.$( '.invalid-title' ).show();\n\t return;\n\t }\n\t // get further settings, shut down and indicate the ajax call, then hide and resolve/reject\n\t var copyAllDatasets = modal.$( 'input[name=\"copy-what\"]:checked' ).val() === 'copy-all';\n\t modal.$( 'button' ).prop( 'disabled', true );\n\t dialog._showAjaxIndicator();\n\t history.copy( true, name, copyAllDatasets )\n\t .done( function( response ){\n\t deferred.resolve( response );\n\t })\n\t //TODO: make this unneccessary with pub-sub error or handling via Galaxy\n\t .fail( function(){\n\t alert([ dialog.errorMessage, _l( 'Please contact a Galaxy administrator' ) ].join( '. ' ));\n\t deferred.rejectWith( deferred, arguments );\n\t })\n\t .always( function(){\n\t if( autoClose ){ modal.hide(); }\n\t });\n\t }\n\t\n\t var originalClosingCallback = options.closing_callback;\n\t modal.show( _.extend( options, {\n\t title : this.title({ name: history.get( 'name' ) }),\n\t body : $( dialog._template({\n\t name : defaultCopyName,\n\t isAnon : Galaxy.user.isAnonymous(),\n\t allowAll : allowAll,\n\t copyWhat : defaultCopyWhat,\n\t activeLabel : this.activeLabel,\n\t allLabel : this.allLabel,\n\t anonWarning : this.anonWarning,\n\t })),\n\t buttons : _.object([\n\t [ _l( 'Cancel' ), function(){ modal.hide(); } ],\n\t [ this.submitLabel, checkNameAndCopy ]\n\t ]),\n\t height : 'auto',\n\t closing_events : true,\n\t closing_callback: function _historyCopyClose( cancelled ){\n\t if( cancelled ){\n\t deferred.reject({ cancelled : true });\n\t }\n\t if( originalClosingCallback ){\n\t originalClosingCallback( cancelled );\n\t }\n\t }\n\t }));\n\t\n\t // set the default dataset copy, autofocus the title, and set up for a simple return\n\t modal.$( '#copy-modal-title' ).focus().select();\n\t modal.$( '#copy-modal-title' ).on( 'keydown', function( ev ){\n\t if( ev.keyCode === 13 ){\n\t ev.preventDefault();\n\t checkNameAndCopy();\n\t }\n\t });\n\t\n\t return deferred;\n\t },\n\t};\n\t\n\t//==============================================================================\n\t// maintain the (slight) distinction between copy and import\n\t/**\n\t * Subclass CopyDialog to use the import language.\n\t */\n\tvar ImportDialog = _.extend( {}, CopyDialog, {\n\t defaultName : _.template( \"imported: <%- name %>\" ),\n\t title : _.template( _l( 'Importing history' ) + ' \"<%- name %>\"' ),\n\t submitLabel : _l( 'Import' ),\n\t errorMessage : _l( 'History could not be imported' ),\n\t progressive : _l( 'Importing history' ),\n\t activeLabel : _l( 'Import only the active, non-deleted datasets' ),\n\t allLabel : _l( 'Import all datasets including deleted ones' ),\n\t anonWarning : _l( 'As an anonymous user, unless you login or register, you will lose your current history ' ) +\n\t _l( 'after importing this history. ' ),\n\t\n\t});\n\t\n\t//==============================================================================\n\t/**\n\t * Main interface for both history import and history copy dialogs.\n\t * @param {Backbone.Model} history the history to copy\n\t * @param {Object} options a hash\n\t * @return {jQuery.Deferred} promise that fails on close and succeeds on copy\n\t *\n\t * options:\n\t * (this object is also passed to the modal used to display the dialog and accepts modal options)\n\t * {Function} nameFn if defined, use this to build the default name shown to the user\n\t * (the fn is passed: {name:' +\n\t ' | ' +\n\t ' | Description | ' +\n\t 'Name | ' +\n\t 'Size | ' +\n\t 'Settings | ' +\n\t 'Status | ' +\n\t '
---|
Name | ' +\n\t 'Size | ' +\n\t 'Type | ' +\n\t 'Genome | ' +\n\t 'Settings | ' +\n\t 'Status | ' +\n\t '' +\n\t ' |
---|
\" + this.formattedReference() + \"
\" );\n return this;\n },\n formattedReference: function() {\n var model = this.model;\n var entryType = model.entryType();\n var fields = model.fields();\n\n var ref = \"\";\n // Code inspired by...\n // https://github.com/vkaravir/bib-publication-list/blob/master/src/bib-publication-list.js\n var authorsAndYear = this._asSentence( (fields.author ? fields.author : \"\") + (fields.year ? (\" (\" + fields.year + \")\") : \"\") ) + \" \";\n var title = fields.title || \"\";\n var pages = fields.pages ? (\"pp. \" + fields.pages) : \"\";\n var address = fields.address;\n if( entryType == \"article\" ) {\n var volume = (fields.volume ? fields.volume : \"\") +\n (fields.number ? ( \" (\" + fields.number + \")\" ) : \"\") +\n (pages ? \", \" + pages : \"\");\n ref = authorsAndYear + this._asSentence(title) +\n (fields.journal ? (\"In \" + fields.journal + \", \") : \"\") +\n this._asSentence(volume) + \n this._asSentence(fields.address) +\n \"<\\/em>\";\n } else if( entryType == \"inproceedings\" || entryType == \"proceedings\" ) {\n ref = authorsAndYear + \n this._asSentence(title) + \n (fields.booktitle ? (\"In \" + fields.booktitle + \", \") : \"\") +\n (pages ? pages : \"\") +\n (address ? \", \" + address : \"\") + \n \".<\\/em>\";\n } else if( entryType == \"mastersthesis\" || entryType == \"phdthesis\" ) {\n ref = authorsAndYear + this._asSentence(title) +\n (fields.howpublished ? fields.howpublished + \". \" : \"\") +\n (fields.note ? fields.note + \".\" : \"\");\n } else if( entryType == \"techreport\" ) {\n ref = authorsAndYear + this._asSentence(title) +\n this._asSentence(fields.institution) +\n this._asSentence(fields.number) +\n this._asSentence(fields.type);\n } else if( entryType == \"book\" || entryType == \"inbook\" || entryType == \"incollection\" ) {\n ref = authorsAndYear + \" \" + this._formatBookInfo(fields);\n } else {\n ref = authorsAndYear + \" \" + this._asSentence(title) +\n this._asSentence(fields.howpublished) +\n this._asSentence(fields.note);\n }\n var doiUrl = \"\";\n if( fields.doi ) {\n doiUrl = 'http://dx.doi.org/' + fields.doi;\n ref += '[doi:' + fields.doi + \"]\";\n }\n var url = fields.url || doiUrl;\n if( url ) {\n ref += '[Link]';\n }\n return ref;\n },\n _formatBookInfo: function(fields) {\n var info = \"\";\n if( fields.chapter ) {\n info += fields.chapter + \" in \";\n }\n if( fields.title ) {\n info += \"\" + fields.title + \"<\\/em>\";\n }\n if( fields.editor ) {\n info += \", Edited by \" + fields.editor + \", \";\n }\n if( fields.publisher) {\n info += \", \" + fields.publisher;\n }\n if( fields.pages ) {\n info += \", pp. \" + fields.pages + \"\";\n }\n if( fields.series ) {\n info += \", \" + fields.series + \"<\\/em>\";\n }\n if( fields.volume ) {\n info += \", Vol.\" + fields.volume;\n }\n if( fields.issn ) {\n info += \", ISBN: \" + fields.issn;\n }\n return info + \".\";\n },\n _asSentence: function(str) {\n return (str && str.trim()) ? str + \". \" : \"\";\n }\n});\n\nvar CitationListView = Backbone.View.extend({\n el: '#citations',\n /**\n * Set up view.\n */\n initialize: function() {\n this.listenTo( this.collection, 'add', this.renderCitation );\n },\n\n events: {\n 'click .citations-to-bibtex': 'showBibtex',\n 'click .citations-to-formatted': 'showFormatted'\n },\n\n renderCitation: function( citation ) {\n var citationView = new CitationView( { model: citation } );\n this.$(\".citations-formatted\").append( citationView.render().el );\n var rawTextarea = this.$(\".citations-bibtex-text\");\n rawTextarea.val( rawTextarea.val() + \"\\n\\r\" + citation.attributes.content );\n },\n\n render: function() {\n this.$el.html(this.citationsElement());\n this.collection.each(function( item ){\n this.renderCitation( item );\n }, this);\n this.showFormatted();\n },\n\n showBibtex: function() {\n this.$(\".citations-to-formatted\").show();\n this.$(\".citations-to-bibtex\").hide();\n this.$(\".citations-bibtex\").show();\n this.$(\".citations-formatted\").hide();\n this.$(\".citations-bibtex-text\").select();\n },\n\n showFormatted: function() {\n this.$(\".citations-to-formatted\").hide();\n this.$(\".citations-to-bibtex\").show();\n this.$(\".citations-bibtex\").hide();\n this.$(\".citations-formatted\").show();\n },\n\n partialWarningElement: function() {\n if( this.collection.partial ) {\n return [\n '' + JSON.stringify( xhr.responseJSON ) + '';\n } else {\n content += ': ' + message;\n }\n }\n creator._showAlert( content, 'alert-danger' );\n },\n\n events : {\n // header\n 'click .more-help' : '_clickMoreHelp',\n 'click .less-help' : '_clickLessHelp',\n 'click .main-help' : '_toggleHelp',\n 'click .header .alert button' : '_hideAlert',\n\n 'click .reset' : 'reset',\n 'click .clear-selected' : 'clearSelectedElements',\n\n // elements - selection\n 'click .collection-elements' : 'clearSelectedElements',\n\n // elements - drop target\n // 'dragenter .collection-elements': '_dragenterElements',\n // 'dragleave .collection-elements': '_dragleaveElements',\n 'dragover .collection-elements' : '_dragoverElements',\n 'drop .collection-elements' : '_dropElements',\n\n // these bubble up from the elements as custom events\n 'collection-element.dragstart .collection-elements' : '_elementDragstart',\n 'collection-element.dragend .collection-elements' : '_elementDragend',\n\n // footer\n 'change .collection-name' : '_changeName',\n 'keydown .collection-name' : '_nameCheckForEnter',\n 'click .cancel-create' : function( ev ){\n if( typeof this.oncancel === 'function' ){\n this.oncancel.call( this );\n }\n },\n 'click .create-collection' : '_clickCreate'//,\n },\n\n // ........................................................................ header\n /** expand help */\n _clickMoreHelp : function( ev ){\n ev.stopPropagation();\n this.$( '.main-help' ).addClass( 'expanded' );\n this.$( '.more-help' ).hide();\n },\n /** collapse help */\n _clickLessHelp : function( ev ){\n ev.stopPropagation();\n this.$( '.main-help' ).removeClass( 'expanded' );\n this.$( '.more-help' ).show();\n },\n /** toggle help */\n _toggleHelp : function( ev ){\n ev.stopPropagation();\n this.$( '.main-help' ).toggleClass( 'expanded' );\n this.$( '.more-help' ).toggle();\n },\n\n /** show an alert on the top of the interface containing message (alertClass is bootstrap's alert-*) */\n _showAlert : function( message, alertClass ){\n alertClass = alertClass || 'alert-danger';\n this.$( '.main-help' ).hide();\n this.$( '.header .alert' )\n .attr( 'class', 'alert alert-dismissable' ).addClass( alertClass ).show()\n .find( '.alert-message' ).html( message );\n },\n /** hide the alerts at the top */\n _hideAlert : function( message ){\n this.$( '.main-help' ).show();\n this.$( '.header .alert' ).hide();\n },\n\n // ........................................................................ elements\n /** reset all data to the initial state */\n reset : function(){\n this._instanceSetUp();\n this._elementsSetUp();\n this.render();\n },\n\n /** deselect all elements */\n clearSelectedElements : function( ev ){\n this.$( '.collection-elements .collection-element' ).removeClass( 'selected' );\n this.$( '.collection-elements-controls > .clear-selected' ).hide();\n },\n\n //_dragenterElements : function( ev ){\n // //this.debug( '_dragenterElements:', ev );\n //},\n//TODO: if selected are dragged out of the list area - remove the placeholder - cuz it won't work anyway\n // _dragleaveElements : function( ev ){\n // //this.debug( '_dragleaveElements:', ev );\n // },\n\n /** track the mouse drag over the list adding a placeholder to show where the drop would occur */\n _dragoverElements : function( ev ){\n //this.debug( '_dragoverElements:', ev );\n ev.preventDefault();\n\n var $list = this.$list();\n this._checkForAutoscroll( $list, ev.originalEvent.clientY );\n var $nearest = this._getNearestElement( ev.originalEvent.clientY );\n\n //TODO: no need to re-create - move instead\n this.$( '.element-drop-placeholder' ).remove();\n var $placeholder = $( '' );\n if( !$nearest.length ){\n $list.append( $placeholder );\n } else {\n $nearest.before( $placeholder );\n }\n },\n\n /** If the mouse is near enough to the list's top or bottom, scroll the list */\n _checkForAutoscroll : function( $element, y ){\n var AUTOSCROLL_SPEED = 2,\n offset = $element.offset(),\n scrollTop = $element.scrollTop(),\n upperDist = y - offset.top,\n lowerDist = ( offset.top + $element.outerHeight() ) - y;\n if( upperDist >= 0 && upperDist < this.autoscrollDist ){\n $element.scrollTop( scrollTop - AUTOSCROLL_SPEED );\n } else if( lowerDist >= 0 && lowerDist < this.autoscrollDist ){\n $element.scrollTop( scrollTop + AUTOSCROLL_SPEED );\n }\n },\n\n /** get the nearest element based on the mouse's Y coordinate.\n * If the y is at the end of the list, return an empty jQuery object.\n */\n _getNearestElement : function( y ){\n var WIGGLE = 4,\n lis = this.$( '.collection-elements li.collection-element' ).toArray();\n for( var i=0; i
', _l([\n 'Collections of datasets are permanent, ordered lists of datasets that can be passed to tools and ',\n 'workflows in order to have analyses done on each member of the entire group. This interface allows ',\n 'you to create a collection and re-order the final collection.'\n ].join( '' )), '
',\n '', _l([\n 'Once your collection is complete, enter a name and ',\n 'click \"Create list\".'\n ].join( '' )), '
'\n ].join('')),\n\n /** shown in list when all elements are discarded */\n invalidElements : _.template([\n _l( 'The following selections could not be included due to problems:' ),\n '<%= dataset.peek %>',\n '<% } %>',\n '<% } %>',\n '
' +\n ' | Name | ' +\n 'Size | ' +\n 'Created | ' +\n '
---|
' + JSON.stringify( xhr.responseJSON ) + '';\n } else {\n content += ': ' + message;\n }\n }\n creator._showAlert( content, 'alert-danger' );\n },\n\n // ------------------------------------------------------------------------ rendering\n /** render the entire interface */\n render : function( speed, callback ){\n //this.debug( '-- _render' );\n //this.$el.empty().html( PairedCollectionCreator.templates.main() );\n this.$el.empty().html( PairedCollectionCreator.templates.main() );\n this._renderHeader( speed );\n this._renderMiddle( speed );\n this._renderFooter( speed );\n this._addPluginComponents();\n this.trigger( 'rendered', this );\n return this;\n },\n\n /** render the header section */\n _renderHeader : function( speed, callback ){\n //this.debug( '-- _renderHeader' );\n var $header = this.$( '.header' ).empty().html( PairedCollectionCreator.templates.header() )\n .find( '.help-content' ).prepend( $( PairedCollectionCreator.templates.helpContent() ) );\n\n this._renderFilters();\n return $header;\n },\n /** fill the filter inputs with the filter values */\n _renderFilters : function(){\n return this.$( '.forward-column .column-header input' ).val( this.filters[0] )\n .add( this.$( '.reverse-column .column-header input' ).val( this.filters[1] ) );\n },\n\n /** render the middle including unpaired and paired sections (which may be hidden) */\n _renderMiddle : function( speed, callback ){\n var $middle = this.$( '.middle' ).empty().html( PairedCollectionCreator.templates.middle() );\n\n // (re-) hide the un/paired panels based on instance vars\n if( this.unpairedPanelHidden ){\n this.$( '.unpaired-columns' ).hide();\n } else if( this.pairedPanelHidden ){\n this.$( '.paired-columns' ).hide();\n }\n\n this._renderUnpaired();\n this._renderPaired();\n return $middle;\n },\n /** render the unpaired section, showing datasets accrd. to filters, update the unpaired counts */\n _renderUnpaired : function( speed, callback ){\n //this.debug( '-- _renderUnpaired' );\n var creator = this,\n $fwd, $rev, $prd = [],\n split = this._splitByFilters();\n // update unpaired counts\n this.$( '.forward-column .title' )\n .text([ split[0].length, _l( 'unpaired forward' ) ].join( ' ' ));\n this.$( '.forward-column .unpaired-info' )\n .text( this._renderUnpairedDisplayStr( this.unpaired.length - split[0].length ) );\n this.$( '.reverse-column .title' )\n .text([ split[1].length, _l( 'unpaired reverse' ) ].join( ' ' ));\n this.$( '.reverse-column .unpaired-info' )\n .text( this._renderUnpairedDisplayStr( this.unpaired.length - split[1].length ) );\n\n this.$( '.unpaired-columns .column-datasets' ).empty();\n\n // show/hide the auto pair button if any unpaired are left\n this.$( '.autopair-link' ).toggle( this.unpaired.length !== 0 );\n if( this.unpaired.length === 0 ){\n this._renderUnpairedEmpty();\n return;\n }\n\n // create the dataset dom arrays\n $rev = split[1].map( function( dataset, i ){\n // if there'll be a fwd dataset across the way, add a button to pair the row\n if( ( split[0][ i ] !== undefined )\n && ( split[0][ i ] !== dataset ) ){\n $prd.push( creator._renderPairButton() );\n }\n return creator._renderUnpairedDataset( dataset );\n });\n $fwd = split[0].map( function( dataset ){\n return creator._renderUnpairedDataset( dataset );\n });\n\n if( !$fwd.length && !$rev.length ){\n this._renderUnpairedNotShown();\n return;\n }\n // add to appropo cols\n //TODO: not the best way to render - consider rendering the entire unpaired-columns section in a fragment\n // and swapping out that\n this.$( '.unpaired-columns .forward-column .column-datasets' ).append( $fwd )\n .add( this.$( '.unpaired-columns .paired-column .column-datasets' ).append( $prd ) )\n .add( this.$( '.unpaired-columns .reverse-column .column-datasets' ).append( $rev ) );\n this._adjUnpairedOnScrollbar();\n },\n /** return a string to display the count of filtered out datasets */\n _renderUnpairedDisplayStr : function( numFiltered ){\n return [ '(', numFiltered, ' ', _l( 'filtered out' ), ')' ].join('');\n },\n /** return an unattached jQuery DOM element to represent an unpaired dataset */\n _renderUnpairedDataset : function( dataset ){\n //TODO: to underscore template\n return $( '')\n .attr( 'id', 'dataset-' + dataset.id )\n .addClass( 'dataset unpaired' )\n .attr( 'draggable', true )\n .addClass( dataset.selected? 'selected': '' )\n .append( $( '' ).addClass( 'dataset-name' ).text( dataset.name ) )\n //??\n .data( 'dataset', dataset );\n },\n /** render the button that may go between unpaired datasets, allowing the user to pair a row */\n _renderPairButton : function(){\n //TODO: *not* a dataset - don't pretend like it is\n return $( '').addClass( 'dataset unpaired' )\n .append( $( '' ).addClass( 'dataset-name' ).text( _l( 'Pair these datasets' ) ) );\n },\n /** a message to display when no unpaired left */\n _renderUnpairedEmpty : function(){\n //this.debug( '-- renderUnpairedEmpty' );\n var $msg = $( '' )\n .text( '(' + _l( 'no remaining unpaired datasets' ) + ')' );\n this.$( '.unpaired-columns .paired-column .column-datasets' ).empty().prepend( $msg );\n return $msg;\n },\n /** a message to display when no unpaired can be shown with the current filters */\n _renderUnpairedNotShown : function(){\n //this.debug( '-- renderUnpairedEmpty' );\n var $msg = $( '' )\n .text( '(' + _l( 'no datasets were found matching the current filters' ) + ')' );\n this.$( '.unpaired-columns .paired-column .column-datasets' ).empty().prepend( $msg );\n return $msg;\n },\n /** try to detect if the unpaired section has a scrollbar and adjust left column for better centering of all */\n _adjUnpairedOnScrollbar : function(){\n var $unpairedColumns = this.$( '.unpaired-columns' ).last(),\n $firstDataset = this.$( '.unpaired-columns .reverse-column .dataset' ).first();\n if( !$firstDataset.length ){ return; }\n var ucRight = $unpairedColumns.offset().left + $unpairedColumns.outerWidth(),\n dsRight = $firstDataset.offset().left + $firstDataset.outerWidth(),\n rightDiff = Math.floor( ucRight ) - Math.floor( dsRight );\n //this.debug( 'rightDiff:', ucRight, '-', dsRight, '=', rightDiff );\n this.$( '.unpaired-columns .forward-column' )\n .css( 'margin-left', ( rightDiff > 0 )? rightDiff: 0 );\n },\n\n /** render the paired section and update counts of paired datasets */\n _renderPaired : function( speed, callback ){\n //this.debug( '-- _renderPaired' );\n this.$( '.paired-column-title .title' ).text([ this.paired.length, _l( 'paired' ) ].join( ' ' ) );\n // show/hide the unpair all link\n this.$( '.unpair-all-link' ).toggle( this.paired.length !== 0 );\n if( this.paired.length === 0 ){\n this._renderPairedEmpty();\n return;\n //TODO: would be best to return here (the $columns)\n } else {\n // show/hide 'remove extensions link' when any paired and they seem to have extensions\n this.$( '.remove-extensions-link' ).show();\n }\n\n this.$( '.paired-columns .column-datasets' ).empty();\n var creator = this;\n this.paired.forEach( function( pair, i ){\n //TODO: cache these?\n var pairView = new PairView({ pair: pair });\n creator.$( '.paired-columns .column-datasets' )\n .append( pairView.render().$el )\n .append([\n ''\n ].join( '' ));\n });\n },\n /** a message to display when none paired */\n _renderPairedEmpty : function(){\n var $msg = $( '' )\n .text( '(' + _l( 'no paired datasets yet' ) + ')' );\n this.$( '.paired-columns .column-datasets' ).empty().prepend( $msg );\n return $msg;\n },\n\n /** render the footer, completion controls, and cancel controls */\n _renderFooter : function( speed, callback ){\n var $footer = this.$( '.footer' ).empty().html( PairedCollectionCreator.templates.footer() );\n this.$( '.remove-extensions' ).prop( 'checked', this.removeExtensions );\n if( typeof this.oncancel === 'function' ){\n this.$( '.cancel-create.btn' ).show();\n }\n return $footer;\n },\n\n /** add any jQuery/bootstrap/custom plugins to elements rendered */\n _addPluginComponents : function(){\n this._chooseFiltersPopover( '.choose-filters-link' );\n this.$( '.help-content i' ).hoverhighlight( '.collection-creator', 'rgba( 64, 255, 255, 1.0 )' );\n },\n\n /** build a filter selection popover allowing selection of common filter pairs */\n _chooseFiltersPopover : function( selector ){\n function filterChoice( val1, val2 ){\n return [\n ''\n ].join('');\n }\n var $popoverContent = $( _.template([\n '
', _l([\n 'Collections of paired datasets are ordered lists of dataset pairs (often forward and reverse reads). ',\n 'These collections can be passed to tools and workflows in order to have analyses done on each member of ',\n 'the entire group. This interface allows you to create a collection, choose which datasets are paired, ',\n 'and re-order the final collection.'\n ].join( '' )), '
',\n '', _l([\n 'Unpaired datasets are shown in the unpaired section ',\n '(hover over the underlined words to highlight below). ',\n 'Paired datasets are shown in the paired section.',\n '
', _l([\n '
', _l([\n 'To unpair individual dataset pairs, click the ',\n 'unpair buttons ( ). ',\n 'Click the \"Unpair all\" link to unpair all pairs.'\n ].join( '' )), '
',\n '', _l([\n 'You can include or remove the file extensions (e.g. \".fastq\") from your pair names by toggling the ',\n '\"Remove file extensions from pair names?\" control.'\n ].join( '' )), '
',\n '', _l([\n 'Once your collection is complete, enter a name and ',\n 'click \"Create list\". ',\n '(Note: you do not have to pair all unpaired datasets to finish.)'\n ].join( '' )), '
'\n ].join(''))\n};\n\n\n//=============================================================================\n/** a modal version of the paired collection creator */\nvar pairedCollectionCreatorModal = function _pairedCollectionCreatorModal( datasets, options ){\n\n var deferred = jQuery.Deferred(),\n creator;\n\n options = _.defaults( options || {}, {\n datasets : datasets,\n oncancel : function(){\n Galaxy.modal.hide();\n deferred.reject( 'cancelled' );\n },\n oncreate : function( creator, response ){\n Galaxy.modal.hide();\n deferred.resolve( response );\n }\n });\n\n if( !window.Galaxy || !Galaxy.modal ){\n throw new Error( 'Galaxy or Galaxy.modal not found' );\n }\n\n creator = new PairedCollectionCreator( options );\n Galaxy.modal.show({\n title : 'Create a collection of paired datasets',\n body : creator.$el,\n width : '80%',\n height : '800px',\n closing_events: true\n });\n creator.render();\n window.creator = creator;\n\n //TODO: remove modal header\n return deferred;\n};\n\n\n//=============================================================================\nfunction createListOfPairsCollection( collection ){\n var elements = collection.toJSON();\n//TODO: validate elements\n return pairedCollectionCreatorModal( elements, {\n historyId : collection.historyId\n });\n}\n\n\n//=============================================================================\n return {\n PairedCollectionCreator : PairedCollectionCreator,\n pairedCollectionCreatorModal : pairedCollectionCreatorModal,\n createListOfPairsCollection : createListOfPairsCollection\n };\n});\n\n\n\n/*****************\n ** WEBPACK FOOTER\n ** ./galaxy/scripts/mvc/collection/list-of-pairs-collection-creator.js\n ** module id = 99\n ** module chunks = 3\n **/","define([\n \"mvc/collection/list-collection-creator\",\n \"mvc/history/hdca-model\",\n \"mvc/base-mvc\",\n \"utils/localization\"\n], function( LIST_CREATOR, HDCA, BASE_MVC, _l ){\n\n'use strict';\n\nvar logNamespace = 'collections';\n/*==============================================================================\nTODO:\n the paired creator doesn't really mesh with the list creator as parent\n it may be better to make an abstract super class for both\n composites may inherit from this (or vis-versa)\n PairedDatasetCollectionElementView doesn't make a lot of sense\n\n==============================================================================*/\n/** */\nvar PairedDatasetCollectionElementView = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend({\n _logNamespace : logNamespace,\n\n//TODO: use proper class (DatasetDCE or NestedDCDCE (or the union of both))\n tagName : 'li',\n className : 'collection-element',\n\n initialize : function( attributes ){\n this.element = attributes.element || {};\n this.identifier = attributes.identifier;\n },\n\n render : function(){\n this.$el\n .attr( 'data-element-id', this.element.id )\n .html( this.template({ identifier: this.identifier, element: this.element }) );\n return this;\n },\n\n //TODO: lots of unused space in the element - possibly load details and display them horiz.\n template : _.template([\n '<%- identifier %>',\n '<%- element.name %>',\n ].join('')),\n\n /** remove the DOM and any listeners */\n destroy : function(){\n this.off();\n this.$el.remove();\n },\n\n /** string rep */\n toString : function(){\n return 'DatasetCollectionElementView()';\n }\n});\n\n\n// ============================================================================\nvar _super = LIST_CREATOR.ListCollectionCreator;\n\n/** An interface for building collections.\n */\nvar PairCollectionCreator = _super.extend({\n\n /** the class used to display individual elements */\n elementViewClass : PairedDatasetCollectionElementView,\n /** the class this creator will create and save */\n collectionClass : HDCA.HistoryPairDatasetCollection,\n className : 'pair-collection-creator collection-creator flex-row-container',\n\n /** override to no-op */\n _mangleDuplicateNames : function(){},\n\n // TODO: this whole pattern sucks. There needs to be two classes of problem area:\n // bad inital choices and\n // when the user has painted his/her self into a corner during creation/use-of-the-creator\n /** render the entire interface */\n render : function( speed, callback ){\n if( this.workingElements.length === 2 ){\n return _super.prototype.render.call( this, speed, callback );\n }\n return this._renderInvalid( speed, callback );\n },\n\n // ------------------------------------------------------------------------ rendering elements\n /** render forward/reverse */\n _renderList : function( speed, callback ){\n //this.debug( '-- _renderList' );\n //precondition: there are two valid elements in workingElements\n var creator = this,\n $tmp = jQuery( '' ),\n $list = creator.$list();\n\n // lose the original views, create the new, append all at once, then call their renders\n _.each( this.elementViews, function( view ){\n view.destroy();\n creator.removeElementView( view );\n });\n $tmp.append( creator._createForwardElementView().$el );\n $tmp.append( creator._createReverseElementView().$el );\n $list.empty().append( $tmp.children() );\n _.invoke( creator.elementViews, 'render' );\n },\n\n /** create the forward element view */\n _createForwardElementView : function(){\n return this._createElementView( this.workingElements[0], { identifier: 'forward' } );\n },\n\n /** create the forward element view */\n _createReverseElementView : function(){\n return this._createElementView( this.workingElements[1], { identifier: 'reverse' } );\n },\n\n /** create an element view, cache in elementViews, and return */\n _createElementView : function( element, options ){\n var elementView = new this.elementViewClass( _.extend( options, {\n element : element,\n }));\n this.elementViews.push( elementView );\n return elementView;\n },\n\n /** swap the forward, reverse elements and re-render */\n swap : function(){\n this.workingElements = [\n this.workingElements[1],\n this.workingElements[0],\n ];\n this._renderList();\n },\n\n events : _.extend( _.clone( _super.prototype.events ), {\n 'click .swap' : 'swap',\n }),\n\n // ------------------------------------------------------------------------ templates\n //TODO: move to require text plugin and load these as text\n //TODO: underscore currently unnecc. bc no vars are used\n //TODO: better way of localizing text-nodes in long strings\n /** underscore template fns attached to class */\n templates : _.extend( _.clone( _super.prototype.templates ), {\n /** the middle: element list */\n middle : _.template([\n '', _l([\n 'Pair collections are permanent collections containing two datasets: one forward and one reverse. ',\n 'Often these are forward and reverse reads. The pair collections can be passed to tools and ',\n 'workflows in order to have analyses done on both datasets. This interface allows ',\n 'you to create a pair, name it, and swap which is forward and which reverse.'\n ].join( '' )), '
',\n '', _l([\n 'Once your collection is complete, enter a name and ',\n 'click \"Create list\".'\n ].join( '' )), '
'\n ].join('')),\n\n /** a simplified page communicating what went wrong and why the user needs to reselect something else */\n invalidInitial : _.template([\n '' + this.progressive + '...
';\n this.modal.$( '.modal-body' ).empty().append( indicator ).css({ 'margin-top': '8px' });\n },\n\n // (sorta) public interface - display the modal, render the form, and potentially copy the history\n // returns a jQuery.Deferred done->history copied, fail->user cancelled\n dialog : function _dialog( modal, history, options ){\n options = options || {};\n\n var dialog = this,\n deferred = jQuery.Deferred(),\n // TODO: getting a little byzantine here\n defaultCopyNameFn = options.nameFn || this.defaultName,\n defaultCopyName = defaultCopyNameFn({ name: history.get( 'name' ) }),\n // TODO: these two might be simpler as one 3 state option (all,active,no-choice)\n defaultCopyWhat = options.allDatasets? 'copy-all' : 'copy-non-deleted',\n allowAll = !_.isUndefined( options.allowAll )? options.allowAll : true,\n autoClose = !_.isUndefined( options.autoClose )? options.autoClose : true;\n\n this.modal = modal;\n\n\n // validate the name and copy if good\n function checkNameAndCopy(){\n var name = modal.$( '#copy-modal-title' ).val();\n if( !name ){\n modal.$( '.invalid-title' ).show();\n return;\n }\n // get further settings, shut down and indicate the ajax call, then hide and resolve/reject\n var copyAllDatasets = modal.$( 'input[name=\"copy-what\"]:checked' ).val() === 'copy-all';\n modal.$( 'button' ).prop( 'disabled', true );\n dialog._showAjaxIndicator();\n history.copy( true, name, copyAllDatasets )\n .done( function( response ){\n deferred.resolve( response );\n })\n //TODO: make this unneccessary with pub-sub error or handling via Galaxy\n .fail( function(){\n alert([ dialog.errorMessage, _l( 'Please contact a Galaxy administrator' ) ].join( '. ' ));\n deferred.rejectWith( deferred, arguments );\n })\n .always( function(){\n if( autoClose ){ modal.hide(); }\n });\n }\n\n var originalClosingCallback = options.closing_callback;\n modal.show( _.extend( options, {\n title : this.title({ name: history.get( 'name' ) }),\n body : $( dialog._template({\n name : defaultCopyName,\n isAnon : Galaxy.user.isAnonymous(),\n allowAll : allowAll,\n copyWhat : defaultCopyWhat,\n activeLabel : this.activeLabel,\n allLabel : this.allLabel,\n anonWarning : this.anonWarning,\n })),\n buttons : _.object([\n [ _l( 'Cancel' ), function(){ modal.hide(); } ],\n [ this.submitLabel, checkNameAndCopy ]\n ]),\n height : 'auto',\n closing_events : true,\n closing_callback: function _historyCopyClose( cancelled ){\n if( cancelled ){\n deferred.reject({ cancelled : true });\n }\n if( originalClosingCallback ){\n originalClosingCallback( cancelled );\n }\n }\n }));\n\n // set the default dataset copy, autofocus the title, and set up for a simple return\n modal.$( '#copy-modal-title' ).focus().select();\n modal.$( '#copy-modal-title' ).on( 'keydown', function( ev ){\n if( ev.keyCode === 13 ){\n ev.preventDefault();\n checkNameAndCopy();\n }\n });\n\n return deferred;\n },\n};\n\n//==============================================================================\n// maintain the (slight) distinction between copy and import\n/**\n * Subclass CopyDialog to use the import language.\n */\nvar ImportDialog = _.extend( {}, CopyDialog, {\n defaultName : _.template( \"imported: <%- name %>\" ),\n title : _.template( _l( 'Importing history' ) + ' \"<%- name %>\"' ),\n submitLabel : _l( 'Import' ),\n errorMessage : _l( 'History could not be imported' ),\n progressive : _l( 'Importing history' ),\n activeLabel : _l( 'Import only the active, non-deleted datasets' ),\n allLabel : _l( 'Import all datasets including deleted ones' ),\n anonWarning : _l( 'As an anonymous user, unless you login or register, you will lose your current history ' ) +\n _l( 'after importing this history. ' ),\n\n});\n\n//==============================================================================\n/**\n * Main interface for both history import and history copy dialogs.\n * @param {Backbone.Model} history the history to copy\n * @param {Object} options a hash\n * @return {jQuery.Deferred} promise that fails on close and succeeds on copy\n *\n * options:\n * (this object is also passed to the modal used to display the dialog and accepts modal options)\n * {Function} nameFn if defined, use this to build the default name shown to the user\n * (the fn is passed: {name:' +\n ' | ' +\n ' | Description | ' +\n 'Name | ' +\n 'Size | ' +\n 'Settings | ' +\n 'Status | ' +\n '
---|
Name | ' +\n 'Size | ' +\n 'Type | ' +\n 'Genome | ' +\n 'Settings | ' +\n 'Status | ' +\n '' +\n ' |
---|