4 new commits in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/5edaafeecfed/ Changeset: 5edaafeecfed User: abretaud Date: 2014-11-27 13:18:18+00:00 Summary: Add an API to remove items from tool data tables Affected #: 2 files diff -r 3975b317ffdcbaa6c8dcb4b2b52404bfa34edbc2 -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 lib/galaxy/tools/data/__init__.py --- a/lib/galaxy/tools/data/__init__.py +++ b/lib/galaxy/tools/data/__init__.py @@ -211,6 +211,13 @@ self.add_entry( entry, allow_duplicates=allow_duplicates, persist=persist, persist_on_error=persist_on_error, entry_source=entry_source, **kwd ) return self._loaded_content_version + def _remove_entry(self, values, **kwd): + raise NotImplementedError( "Abstract method" ) + + def remove_entry(self, values, **kwd): + self._remove_entry_and_reload( values,**kwd ) + return self._update_version() + def is_current_version( self, other_version ): return self._loaded_content_version == other_version @@ -506,6 +513,42 @@ data_table_fh.write( "%s\n" % ( self.separator.join( fields ) ) ) return not is_error + def _remove_entry_and_reload( self, values): + + # update every file + for filename in self.filenames: + + if os.path.exists( filename ): + values = self._replace_field_separators( values ) + self.filter_file_fields( filename, values ) + else: + log.warn( "Cannot find index file '%s' for tool data table '%s'" % ( filename, self.name ) ) + + self.reload_from_files() + + def filter_file_fields( self, loc_file, values ): + """ + Reads separated lines from file and print back only the lines that pass a filter. + """ + separator_char = (lambda c: '<TAB>' if c == '\t' else c)(self.separator) + + with open(loc_file) as reader: + rval = "" + for i, line in enumerate( reader ): + if line.lstrip().startswith( self.comment_char ): + rval += line + else: + line_s = line.rstrip( "\n\r" ) + if line_s: + fields = line_s.split( self.separator ) + if fields != values: + rval += line + + with open(loc_file, 'wb') as writer: + writer.write(rval) + + return rval + def _replace_field_separators( self, fields, separator=None, replace=None, comment_char=None ): #make sure none of the fields contain separator #make sure separator replace is different from comment_char, diff -r 3975b317ffdcbaa6c8dcb4b2b52404bfa34edbc2 -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 lib/galaxy/webapps/galaxy/api/tool_data.py --- a/lib/galaxy/webapps/galaxy/api/tool_data.py +++ b/lib/galaxy/webapps/galaxy/api/tool_data.py @@ -20,3 +20,44 @@ @web.expose_api def show( self, trans, id, **kwds ): return trans.app.tool_data_tables.data_tables[id].to_dict(view='element') + + @web.require_admin + @web.expose_api + def delete( self, trans, id, **kwd ): + """ + DELETE /api/tool_data/{id} + Removes a role from a group + + :type id: str + :param id: the encoded id of the history to delete + :type kwd: dict + :param kwd: (required) dictionary structure containing: + + * payload: a dictionary itself containing: + * values: <TAB> separated list of column contents, there must be a value for all the columns of the data table + """ + decoded_tool_data_id = id + + try: + data_table = trans.app.tool_data_tables.data_tables.get(decoded_tool_data_id) + except: + data_table = None + if not data_table: + trans.response.status = 400 + return "Invalid data table id ( %s ) specified." % str( decoded_tool_data_id ) + + values = None + if kwd.get( 'payload', None ): + values = kwd['payload'].get( 'values', '' ) + + if not values: + trans.response.status = 400 + return "Invalid data table item ( %s ) specified." % str( values ) + + split_values = values.split("\t") + + if len(split_values) != len(data_table.get_column_name_list()): + trans.response.status = 400 + return "Invalid data table item ( %s ) specified. Wrong number of columns (%s given, %s required)." % ( str( values ), str(len(split_values)), str(len(data_table.get_column_name_list()))) + + return data_table.remove_entry(split_values) https://bitbucket.org/galaxy/galaxy-central/commits/d76253bb22c4/ Changeset: d76253bb22c4 User: abretaud Date: 2014-12-09 14:01:47+00:00 Summary: Merged galaxy/galaxy-central into default Affected #: 184 files diff -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 -r d76253bb22c4f64c73523956d119c0c5ab0a357b .hgtags --- a/.hgtags +++ b/.hgtags @@ -20,4 +20,4 @@ ca45b78adb4152fc6e7395514d46eba6b7d0b838 release_2014.08.11 548ab24667d6206780237bd807f7d857a484c461 latest_2014.08.11 2092948937ac30ef82f71463a235c66d34987088 release_2014.10.06 -acc8d1e2bc88530aa8d8651cf5f88649f6769304 latest_2014.10.06 +acb2548443ae42d39ef200d035ccc0481d6b930c latest_2014.10.06 diff -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 -r d76253bb22c4f64c73523956d119c0c5ab0a357b client/galaxy/scripts/galaxy.library.js --- a/client/galaxy/scripts/galaxy.library.js +++ b/client/galaxy/scripts/galaxy.library.js @@ -36,7 +36,7 @@ initialize: function() { this.routesHit = 0; //keep count of number of routes handled by the application - Backbone.history.on('route', function() { this.routesHit++; }, this); + Backbone.history.on( 'route', function() { this.routesHit++; }, this ); }, routes: { @@ -45,6 +45,7 @@ "library/:library_id/permissions" : "library_permissions", "folders/:folder_id/permissions" : "folder_permissions", "folders/:id" : "folder_content", + "folders/:id/page/:show_page" : "folder_page", "folders/:folder_id/datasets/:dataset_id" : "dataset_detail", "folders/:folder_id/datasets/:dataset_id/permissions" : "dataset_permissions", "folders/:folder_id/datasets/:dataset_id/versions/:ldda_id" : "dataset_version", @@ -53,13 +54,13 @@ }, back: function() { - if(this.routesHit > 1) { + if( this.routesHit > 1 ) { //more than one route hit -> user did not land to current page directly window.history.back(); } else { //otherwise go to the home page. Use replaceState if available so //the navigation doesn't create an extra history entry - this.navigate('#', {trigger:true, replace:true}); + this.navigate( '#', { trigger:true, replace:true } ); } } }); @@ -71,7 +72,8 @@ with_deleted : false, sort_order : 'asc', sort_by : 'name', - library_page_size : 20 + library_page_size : 20, + folder_page_size : 15 } }); @@ -94,10 +96,10 @@ this.library_router = new LibraryRouter(); - this.library_router.on('route:libraries', function() { - Galaxy.libraries.libraryToolbarView = new mod_librarytoolbar_view.LibraryToolbarView(); - Galaxy.libraries.libraryListView = new mod_librarylist_view.LibraryListView(); - }); + this.library_router.on( 'route:libraries', function() { + Galaxy.libraries.libraryToolbarView = new mod_librarytoolbar_view.LibraryToolbarView(); + Galaxy.libraries.libraryListView = new mod_librarylist_view.LibraryListView(); + }); this.library_router.on('route:libraries_page', function( show_page ) { if ( Galaxy.libraries.libraryToolbarView === null ){ @@ -108,66 +110,77 @@ } }); - this.library_router.on('route:folder_content', function(id) { + this.library_router.on( 'route:folder_content', function( id ) { if (Galaxy.libraries.folderToolbarView){ - Galaxy.libraries.folderToolbarView.$el.unbind('click'); - } - Galaxy.libraries.folderToolbarView = new mod_foldertoolbar_view.FolderToolbarView({id: id}); - Galaxy.libraries.folderListView = new mod_folderlist_view.FolderListView({id: id}); - }); + Galaxy.libraries.folderToolbarView.$el.unbind( 'click' ); + } + Galaxy.libraries.folderToolbarView = new mod_foldertoolbar_view.FolderToolbarView( { id: id } ); + Galaxy.libraries.folderListView = new mod_folderlist_view.FolderListView( { id: id } ); + }); - this.library_router.on('route:download', function(folder_id, format) { - if ($('#folder_list_body').find(':checked').length === 0) { - mod_toastr.info( 'You must select at least one dataset to download' ); - Galaxy.libraries.library_router.navigate('folders/' + folder_id, {trigger: true, replace: true}); - } else { - Galaxy.libraries.folderToolbarView.download(folder_id, format); - Galaxy.libraries.library_router.navigate('folders/' + folder_id, {trigger: false, replace: true}); - } - }); + this.library_router.on( 'route:folder_page', function( id, show_page ) { + if ( Galaxy.libraries.folderToolbarView === null ){ + Galaxy.libraries.folderToolbarView = new mod_foldertoolbar_view.FolderToolbarView( {id: id} ); + Galaxy.libraries.folderListView = new mod_folderlist_view.FolderListView( { id: id, show_page: show_page } ); + } else { + Galaxy.libraries.folderListView.render( { id: id, show_page: parseInt( show_page ) } ) + } + }); - this.library_router.on('route:dataset_detail', function(folder_id, dataset_id){ - if (Galaxy.libraries.datasetView){ - Galaxy.libraries.datasetView.$el.unbind('click'); - } - Galaxy.libraries.datasetView = new mod_library_dataset_view.LibraryDatasetView({id: dataset_id}); - }); - this.library_router.on('route:dataset_version', function(folder_id, dataset_id, ldda_id){ - if (Galaxy.libraries.datasetView){ - Galaxy.libraries.datasetView.$el.unbind('click'); - } - Galaxy.libraries.datasetView = new mod_library_dataset_view.LibraryDatasetView({id: dataset_id, ldda_id: ldda_id, show_version: true}); - }); + this.library_router.on( 'route:download', function( folder_id, format ) { + if ( $( '#folder_list_body' ).find( ':checked' ).length === 0 ) { + mod_toastr.info( 'You must select at least one dataset to download' ); + Galaxy.libraries.library_router.navigate( 'folders/' + folder_id, { trigger: true, replace: true } ); + } else { + Galaxy.libraries.folderToolbarView.download( folder_id, format ); + Galaxy.libraries.library_router.navigate( 'folders/' + folder_id, { trigger: false, replace: true } ); + } + }); - this.library_router.on('route:dataset_permissions', function(folder_id, dataset_id){ - if (Galaxy.libraries.datasetView){ - Galaxy.libraries.datasetView.$el.unbind('click'); - } - Galaxy.libraries.datasetView = new mod_library_dataset_view.LibraryDatasetView({id: dataset_id, show_permissions: true}); - }); + this.library_router.on( 'route:dataset_detail', function(folder_id, dataset_id){ + if (Galaxy.libraries.datasetView){ + Galaxy.libraries.datasetView.$el.unbind('click'); + } + Galaxy.libraries.datasetView = new mod_library_dataset_view.LibraryDatasetView({id: dataset_id}); + }); - this.library_router.on('route:library_permissions', function(library_id){ - if (Galaxy.libraries.libraryView){ - Galaxy.libraries.libraryView.$el.unbind('click'); - } - Galaxy.libraries.libraryView = new mod_library_library_view.LibraryView({id: library_id, show_permissions: true}); - }); + this.library_router.on( 'route:dataset_version', function(folder_id, dataset_id, ldda_id){ + if (Galaxy.libraries.datasetView){ + Galaxy.libraries.datasetView.$el.unbind('click'); + } + Galaxy.libraries.datasetView = new mod_library_dataset_view.LibraryDatasetView({id: dataset_id, ldda_id: ldda_id, show_version: true}); + }); - this.library_router.on('route:folder_permissions', function(folder_id){ - if (Galaxy.libraries.folderView){ - Galaxy.libraries.folderView.$el.unbind('click'); - } - Galaxy.libraries.folderView = new mod_library_folder_view.FolderView({id: folder_id, show_permissions: true}); - }); - this.library_router.on('route:import_datasets', function(folder_id, source){ - if (Galaxy.libraries.folderToolbarView && Galaxy.libraries.folderListView){ - Galaxy.libraries.folderToolbarView.showImportModal({source:source}); - } else { - Galaxy.libraries.folderToolbarView = new mod_foldertoolbar_view.FolderToolbarView({id: folder_id}); - Galaxy.libraries.folderListView = new mod_folderlist_view.FolderListView({id: folder_id}); - Galaxy.libraries.folderToolbarView.showImportModal({source: source}); - } - }); + this.library_router.on( 'route:dataset_permissions', function(folder_id, dataset_id){ + if (Galaxy.libraries.datasetView){ + Galaxy.libraries.datasetView.$el.unbind('click'); + } + Galaxy.libraries.datasetView = new mod_library_dataset_view.LibraryDatasetView({id: dataset_id, show_permissions: true}); + }); + + this.library_router.on( 'route:library_permissions', function(library_id){ + if (Galaxy.libraries.libraryView){ + Galaxy.libraries.libraryView.$el.unbind('click'); + } + Galaxy.libraries.libraryView = new mod_library_library_view.LibraryView({id: library_id, show_permissions: true}); + }); + + this.library_router.on( 'route:folder_permissions', function(folder_id){ + if (Galaxy.libraries.folderView){ + Galaxy.libraries.folderView.$el.unbind('click'); + } + Galaxy.libraries.folderView = new mod_library_folder_view.FolderView({id: folder_id, show_permissions: true}); + }); + + this.library_router.on( 'route:import_datasets', function( folder_id, source ){ + if ( Galaxy.libraries.folderToolbarView && Galaxy.libraries.folderListView ){ + Galaxy.libraries.folderToolbarView.showImportModal( { source:source } ); + } else { + Galaxy.libraries.folderToolbarView = new mod_foldertoolbar_view.FolderToolbarView( { id: folder_id } ); + Galaxy.libraries.folderListView = new mod_folderlist_view.FolderListView( { id: folder_id } ); + Galaxy.libraries.folderToolbarView.showImportModal( { source: source } ); + } + }); Backbone.history.start({pushState: false}); } diff -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 -r d76253bb22c4f64c73523956d119c0c5ab0a357b client/galaxy/scripts/jq-plugins/ui/filter-control.js --- /dev/null +++ b/client/galaxy/scripts/jq-plugins/ui/filter-control.js @@ -0,0 +1,204 @@ +// from: https://raw.githubusercontent.com/umdjs/umd/master/jqueryPlugin.js +// Uses AMD or browser globals to create a jQuery plugin. +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else { + // Browser globals + factory(jQuery); + } + +}(function ($) { + //============================================================================== + /** + * Template function that produces a bootstrap dropdown to replace the + * vanilla HTML select input. Pass in an array of options and an initial selection: + * $( '.my-div' ).append( dropDownSelect( [ 'option1', 'option2' ], 'option2' ); + * + * When the user changes the selected option a 'change.dropdown-select' event will + * fire with both the jq event and the new selection text as arguments. + * + * Get the currently selected choice using: + * var userChoice = $( '.my-div .dropdown-select .dropdown-select-selected' ).text(); + * + */ + function dropDownSelect( options, selected ){ + // replacement for vanilla select element using bootstrap dropdowns instead + selected = selected || (( !_.isEmpty( options ) )?( options[0] ):( '' )); + var $select = $([ + '<div class="dropdown-select btn-group">', + '<button type="button" class="btn btn-default">', + '<span class="dropdown-select-selected">' + selected + '</span>', + '</button>', + '</div>' + ].join( '\n' )); + + // if there's only one option, do not style/create as buttons, dropdown - use simple span + // otherwise, a dropdown displaying the current selection + if( options && options.length > 1 ){ + $select.find( 'button' ) + .addClass( 'dropdown-toggle' ).attr( 'data-toggle', 'dropdown' ) + .append( ' <span class="caret"></span>' ); + $select.append([ + '<ul class="dropdown-menu" role="menu">', + _.map( options, function( option ){ + return [ + '<li><a href="javascript:void(0)">', option, '</a></li>' + ].join( '' ); + }).join( '\n' ), + '</ul>' + ].join( '\n' )); + } + + // trigger 'change.dropdown-select' when a new selection is made using the dropdown + function selectThis( event ){ + var $this = $( this ), + $select = $this.parents( '.dropdown-select' ), + newSelection = $this.text(); + $select.find( '.dropdown-select-selected' ).text( newSelection ); + $select.trigger( 'change.dropdown-select', newSelection ); + } + + $select.find( 'a' ).click( selectThis ); + return $select; + } + + //============================================================================== + /** + * Creates a three part bootstrap button group (key, op, value) meant to + * allow the user control of filters (e.g. { key: 'name', op: 'contains', value: 'my_history' }) + * + * Each field uses a dropDownSelect (from ui.js) to allow selection + * (with the 'value' field appearing as an input when set to do so). + * + * Any change or update in any of the fields will trigger a 'change.filter-control' + * event which will be passed an object containing those fields (as the example above). + * + * Pass in an array of possible filter objects to control what the user can select. + * Each filter object should have: + * key : generally the attribute name on which to filter something + * ops : an array of 1 or more filter operations (e.g. [ 'is', '<', 'contains', '!=' ]) + * values (optional) : an array of possible values for the filter (e.g. [ 'true', 'false' ]) + * @example: + * $( '.my-div' ).filterControl({ + * filters : [ + * { key: 'name', ops: [ 'is exactly', 'contains' ] } + * { key: 'deleted', ops: [ 'is' ], values: [ 'true', 'false' ] } + * ] + * }); + * // after initialization, you can prog. get the current value using: + * $( '.my-div' ).filterControl( 'val' ) + * + */ + function FilterControl( element, options ){ + return this.init( element, options ); + } + /** the data key that this object will be stored under in the DOM element */ + FilterControl.prototype.DATA_KEY = 'filter-control'; + + /** parses options, sets up instance vars, and does initial render */ + FilterControl.prototype.init = function _init( element, options ){ + options = options || { filters: [] }; + this.$element = $( element ).addClass( 'filter-control btn-group' ); + this.options = jQuery.extend( true, {}, this.defaults, options ); + + this.currFilter = this.options.filters[0]; + return this.render(); + }; + + /** render (or re-render) the controls on the element */ + FilterControl.prototype.render = function _render(){ + this.$element.empty() + .append([ this._renderKeySelect(), this._renderOpSelect(), this._renderValueInput() ]); + return this; + }; + + /** render the key dropDownSelect, bind a change event to it, and return it */ + FilterControl.prototype._renderKeySelect = function __renderKeySelect(){ + var filterControl = this; + var keys = this.options.filters.map( function( filter ){ + return filter.key; + }); + this.$keySelect = dropDownSelect( keys, this.currFilter.key ) + .addClass( 'filter-control-key' ) + .on( 'change.dropdown-select', function( event, selection ){ + filterControl.currFilter = _.findWhere( filterControl.options.filters, { key: selection }); + // when the filter/key changes, re-render the control entirely + filterControl.render()._triggerChange(); + }); + return this.$keySelect; + }; + + /** render the op dropDownSelect, bind a change event to it, and return it */ + FilterControl.prototype._renderOpSelect = function __renderOpSelect(){ + var filterControl = this, + ops = this.currFilter.ops; + //TODO: search for currOp in avail. ops: use that for selected if there; otherwise: first op + this.$opSelect = dropDownSelect( ops, ops[0] ) + .addClass( 'filter-control-op' ) + .on( 'change.dropdown-select', function( event, selection ){ + filterControl._triggerChange(); + }); + return this.$opSelect; + }; + + /** render the value control, bind a change event to it, and return it */ + FilterControl.prototype._renderValueInput = function __renderValueInput(){ + var filterControl = this; + // if a values attribute is prov. on the filter - make this a dropdown; otherwise, use an input + if( this.currFilter.values ){ + this.$valueSelect = dropDownSelect( this.currFilter.values, this.currFilter.values[0] ) + .on( 'change.dropdown-select', function( event, selection ){ + filterControl._triggerChange(); + }); + } else { + //TODO: allow setting a value type (mainly for which html5 input to use: range, number, etc.) + this.$valueSelect = $( '<input/>' ).addClass( 'form-control' ) + .on( 'change', function( event, value ){ + filterControl._triggerChange(); + }); + } + this.$valueSelect.addClass( 'filter-control-value' ); + return this.$valueSelect; + }; + + /** return the current state/setting for the filter as a three key object: key, op, value */ + FilterControl.prototype.val = function _val(){ + var key = this.$element.find( '.filter-control-key .dropdown-select-selected' ).text(), + op = this.$element.find( '.filter-control-op .dropdown-select-selected' ).text(), + // handle either a dropdown or plain input + $value = this.$element.find( '.filter-control-value' ), + value = ( $value.hasClass( 'dropdown-select' ) )?( $value.find( '.dropdown-select-selected' ).text() ) + :( $value.val() ); + return { key: key, op: op, value: value }; + }; + + // single point of change for change event + FilterControl.prototype._triggerChange = function __triggerChange(){ + this.$element.trigger( 'change.filter-control', this.val() ); + }; + + // as jq plugin + jQuery.fn.extend({ + filterControl : function $filterControl( options ){ + var nonOptionsArgs = jQuery.makeArray( arguments ).slice( 1 ); + return this.map( function(){ + var $this = $( this ), + data = $this.data( FilterControl.prototype.DATA_KEY ); + + if( jQuery.type( options ) === 'object' ){ + data = new FilterControl( $this, options ); + $this.data( FilterControl.prototype.DATA_KEY, data ); + } + if( data && jQuery.type( options ) === 'string' ){ + var fn = data[ options ]; + if( jQuery.type( fn ) === 'function' ){ + return fn.apply( data, nonOptionsArgs ); + } + } + return this; + }); + } + }); +})); diff -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 -r d76253bb22c4f64c73523956d119c0c5ab0a357b client/galaxy/scripts/jq-plugins/ui/mode-button.js --- /dev/null +++ b/client/galaxy/scripts/jq-plugins/ui/mode-button.js @@ -0,0 +1,191 @@ +// from: https://raw.githubusercontent.com/umdjs/umd/master/jqueryPlugin.js +// Uses AMD or browser globals to create a jQuery plugin. +(function (factory) { + if (typeof define === 'function' && define.amd) { + //TODO: So...this turns out to be an all or nothing thing. If I load jQuery in the define below, it will + // (of course) wipe the old jquery *and all the plugins loaded into it*. So the define below *is still + // relying on jquery being loaded globally* in order to preserve plugins. + define([], factory); + } else { + // Browser globals + factory(jQuery); + } + +}(function () { + + /** Multi 'mode' button (or any element really) that changes the html + * contents of itself when clicked. Pass in an ordered list of + * objects with 'html' and (optional) onclick functions. + * + * When clicked in a particular node, the onclick function will + * be called (with the element as this) and the element will + * switch to the next mode, replacing its html content with + * that mode's html. + * + * If there is no next mode, the element will switch back to + * the first mode. + * @example: + * $( '.myElement' ).modeButton({ + * modes : [ + * { + * mode: 'bler', + * html: '<h5>Bler</h5>', + * onclick : function(){ + * $( 'body' ).css( 'background-color', 'red' ); + * } + * }, + * { + * mode: 'bloo', + * html: '<h4>Bloo</h4>', + * onclick : function(){ + * $( 'body' ).css( 'background-color', 'blue' ); + * } + * }, + * { + * mode: 'blah', + * html: '<h3>Blah</h3>', + * onclick : function(){ + * $( 'body' ).css( 'background-color', 'grey' ); + * } + * }, + * ] + * }); + * $( '.myElement' ).modeButton( 'callModeFn', 'bler' ); + */ + /** constructor */ + function ModeButton( element, options ){ + this.currModeIndex = 0; + return this._init( element, options ); + } + + /** html5 data key to store this object inside an element */ + ModeButton.prototype.DATA_KEY = 'mode-button'; + /** default options */ + ModeButton.prototype.defaults = { + switchModesOnClick : true + }; + + // ---- private interface + /** set up options, intial mode, and the click handler */ + ModeButton.prototype._init = function _init( element, options ){ + //console.debug( 'ModeButton._init:', element, options ); + options = options || {}; + this.$element = $( element ); + this.options = $.extend( true, {}, this.defaults, options ); + if( !options.modes ){ + throw new Error( 'ModeButton requires a "modes" array' ); + } + + var modeButton = this; + this.$element.click( function _ModeButtonClick( event ){ + // call the curr mode fn + modeButton.callModeFn(); + // inc the curr mode index + if( modeButton.options.switchModesOnClick ){ modeButton._incModeIndex(); } + // set the element html + $( this ).html( modeButton.options.modes[ modeButton.currModeIndex ].html ); + }); + return this.reset(); + }; + /** increment the mode index to the next in the array, looping back to zero if at the last */ + ModeButton.prototype._incModeIndex = function _incModeIndex(){ + this.currModeIndex += 1; + if( this.currModeIndex >= this.options.modes.length ){ + this.currModeIndex = 0; + } + return this; + }; + /** get the mode index in the modes array for the given key (mode name) */ + ModeButton.prototype._getModeIndex = function _getModeIndex( modeKey ){ + for( var i=0; i<this.options.modes.length; i+=1 ){ + if( this.options.modes[ i ].mode === modeKey ){ return i; } + } + throw new Error( 'mode not found: ' + modeKey ); + }; + /** set the current mode to the one with the given index and set button html */ + ModeButton.prototype._setModeByIndex = function _setModeByIndex( index ){ + var newMode = this.options.modes[ index ]; + if( !newMode ){ + throw new Error( 'mode index not found: ' + index ); + } + this.currModeIndex = index; + if( newMode.html ){ + this.$element.html( newMode.html ); + } + return this; + }; + + // ---- public interface + /** get the current mode object (not just the mode name) */ + ModeButton.prototype.currentMode = function currentMode(){ + return this.options.modes[ this.currModeIndex ]; + }; + /** return the mode key of the current mode */ + ModeButton.prototype.current = function current(){ + // sugar for returning mode name + return this.currentMode().mode; + }; + /** get the mode with the given modeKey or the current mode if modeKey is undefined */ + ModeButton.prototype.getMode = function getMode( modeKey ){ + if( !modeKey ){ return this.currentMode(); } + return this.options.modes[( this._getModeIndex( modeKey ) )]; + }; + /** T/F if the button has the given mode */ + ModeButton.prototype.hasMode = function hasMode( modeKey ){ + try { + return !!this.getMode( modeKey ); + } catch( err ){} + return false; + }; + /** set the current mode to the mode with the given name */ + ModeButton.prototype.setMode = function setMode( modeKey ){ + return this._setModeByIndex( this._getModeIndex( modeKey ) ); + }; + /** reset to the initial mode */ + ModeButton.prototype.reset = function reset(){ + this.currModeIndex = 0; + if( this.options.initialMode ){ + this.currModeIndex = this._getModeIndex( this.options.initialMode ); + } + return this._setModeByIndex( this.currModeIndex ); + }; + /** manually call the click handler of the given mode */ + ModeButton.prototype.callModeFn = function callModeFn( modeKey ){ + var modeFn = this.getMode( modeKey ).onclick; + if( modeFn && $.type( modeFn === 'function' ) ){ + // call with the element as context (std jquery pattern) + return modeFn.call( this.$element.get(0) ); + } + return undefined; + }; + + // as jq plugin + $.fn.modeButton = function $modeButton( options ){ + if( !this.size() ){ return this; } + + //TODO: does map still work with jq multi selection (i.e. $( '.class-for-many-btns' ).modeButton)? + if( $.type( options ) === 'object' ){ + return this.map( function(){ + var $this = $( this ); + $this.data( 'mode-button', new ModeButton( $this, options ) ); + return this; + }); + } + + var $first = $( this[0] ), + button = $first.data( 'mode-button' ); + + if( !button ){ + throw new Error( 'modeButton needs an options object or string name of a function' ); + } + + if( button && $.type( options ) === 'string' ){ + var fnName = options; + if( button && $.type( button[ fnName ] ) === 'function' ){ + return button[ fnName ].apply( button, $.makeArray( arguments ).slice( 1 ) ); + } + } + return button; + }; + +})); diff -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 -r d76253bb22c4f64c73523956d119c0c5ab0a357b client/galaxy/scripts/jq-plugins/ui/pagination.js --- /dev/null +++ b/client/galaxy/scripts/jq-plugins/ui/pagination.js @@ -0,0 +1,226 @@ +// from: https://raw.githubusercontent.com/umdjs/umd/master/jqueryPlugin.js +// Uses AMD or browser globals to create a jQuery plugin. +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else { + // Browser globals + factory(jQuery); + } + +}(function ($) { + + /** Builds (twitter bootstrap styled) pagination controls. + * If the totalDataSize is not null, a horizontal list of page buttons is displayed. + * If totalDataSize is null, two links ('Prev' and 'Next) are displayed. + * When pages are changed, a 'pagination.page-change' event is fired + * sending the event and the (0-based) page requested. + */ + function Pagination( element, options ){ + /** the total number of pages */ + this.numPages = null; + /** the current, active page */ + this.currPage = 0; + return this.init( element, options ); + } + + /** data key under which this object will be stored in the element */ + Pagination.prototype.DATA_KEY = 'pagination'; + /** default options */ + Pagination.prototype.defaults = { + /** which page to begin at */ + startingPage : 0, + /** number of data per page */ + perPage : 20, + /** the total number of data (null == unknown) */ + totalDataSize : null, + /** size of current data on current page */ + currDataSize : null + }; + + /** init the control, calc numPages if possible, and render + * @param {jQuery} the element that will contain the pagination control + * @param {Object} options a map containing overrides to the pagination default options + */ + Pagination.prototype.init = function _init( $element, options ){ + options = options || {}; + this.$element = $element; + this.options = jQuery.extend( true, {}, this.defaults, options ); + + this.currPage = this.options.startingPage; + if( this.options.totalDataSize !== null ){ + this.numPages = Math.ceil( this.options.totalDataSize / this.options.perPage ); + // limit currPage by numPages + if( this.currPage >= this.numPages ){ + this.currPage = this.numPages - 1; + } + } + //console.debug( 'Pagination.prototype.init:', this.$element, this.currPage ); + //console.debug( JSON.stringify( this.options ) ); + + // bind to data of element + this.$element.data( Pagination.prototype.DATA_KEY, this ); + + this._render(); + return this; + }; + + /** helper to create a simple li + a combo */ + function _make$Li( contents ){ + return $([ + '<li><a href="javascript:void(0);">', contents, '</a></li>' + ].join( '' )); + } + + /** render previous and next pagination buttons */ + Pagination.prototype._render = function __render(){ + // no data - no pagination + if( this.options.totalDataSize === 0 ){ return this; } + // only one page + if( this.numPages === 1 ){ return this; } + + // when the number of pages are known, render each page as a link + if( this.numPages > 0 ){ + this._renderPages(); + this._scrollToActivePage(); + + // when the number of pages is not known, render previous or next + } else { + this._renderPrevNext(); + } + return this; + }; + + /** render previous and next pagination buttons */ + Pagination.prototype._renderPrevNext = function __renderPrevNext(){ + var pagination = this, + $prev = _make$Li( 'Prev' ), + $next = _make$Li( 'Next' ), + $paginationContainer = $( '<ul/>' ).addClass( 'pagination pagination-prev-next' ); + + // disable if it either end + if( this.currPage === 0 ){ + $prev.addClass( 'disabled' ); + } else { + $prev.click( function(){ pagination.prevPage(); }); + } + if( ( this.numPages && this.currPage === ( this.numPages - 1 ) ) + || ( this.options.currDataSize && this.options.currDataSize < this.options.perPage ) ){ + $next.addClass( 'disabled' ); + } else { + $next.click( function(){ pagination.nextPage(); }); + } + + this.$element.html( $paginationContainer.append([ $prev, $next ]) ); + //console.debug( this.$element, this.$element.html() ); + return this.$element; + }; + + /** render page links for each possible page (if we can) */ + Pagination.prototype._renderPages = function __renderPages(){ + // it's better to scroll the control and let the user see all pages + // than to force her/him to change pages in order to find the one they want (as traditional << >> does) + var pagination = this, + $scrollingContainer = $( '<div>' ).addClass( 'pagination-scroll-container' ), + $paginationContainer = $( '<ul/>' ).addClass( 'pagination pagination-page-list' ), + page$LiClick = function( ev ){ + pagination.goToPage( $( this ).data( 'page' ) ); + }; + + for( var i=0; i<this.numPages; i+=1 ){ + // add html5 data tag 'page' for later click event handler use + var $pageLi = _make$Li( i + 1 ).attr( 'data-page', i ).click( page$LiClick ); + // highlight the current page + if( i === this.currPage ){ + $pageLi.addClass( 'active' ); + } + //console.debug( '\t', $pageLi ); + $paginationContainer.append( $pageLi ); + } + return this.$element.html( $scrollingContainer.html( $paginationContainer ) ); + }; + + /** scroll scroll-container (if any) to show the active page */ + Pagination.prototype._scrollToActivePage = function __scrollToActivePage(){ + // scroll to show active page in center of scrollable area + var $container = this.$element.find( '.pagination-scroll-container' ); + // no scroll container : don't scroll + if( !$container.size() ){ return this; } + + var $activePage = this.$element.find( 'li.active' ), + midpoint = $container.width() / 2; + //console.debug( $container, $activePage, midpoint ); + $container.scrollLeft( $container.scrollLeft() + $activePage.position().left - midpoint ); + return this; + }; + + /** go to a certain page */ + Pagination.prototype.goToPage = function goToPage( page ){ + if( page <= 0 ){ page = 0; } + if( this.numPages && page >= this.numPages ){ page = this.numPages - 1; } + if( page === this.currPage ){ return this; } + + //console.debug( '\t going to page ' + page ) + this.currPage = page; + this.$element.trigger( 'pagination.page-change', this.currPage ); + //console.info( 'pagination:page-change', this.currPage ); + this._render(); + return this; + }; + + /** go to the previous page */ + Pagination.prototype.prevPage = function prevPage(){ + return this.goToPage( this.currPage - 1 ); + }; + + /** go to the next page */ + Pagination.prototype.nextPage = function nextPage(){ + return this.goToPage( this.currPage + 1 ); + }; + + /** return the current page */ + Pagination.prototype.page = function page(){ + return this.currPage; + }; + + // alternate constructor invocation + Pagination.create = function _create( $element, options ){ + return new Pagination( $element, options ); + }; + + // as jq plugin + jQuery.fn.extend({ + pagination : function $pagination( options ){ + var nonOptionsArgs = jQuery.makeArray( arguments ).slice( 1 ); + + // if passed an object - use that as an options map to create pagination for each selected + if( jQuery.type( options ) === 'object' ){ + return this.map( function(){ + Pagination.create( $( this ), options ); + return this; + }); + } + + // (other invocations only work on the first element in selected) + var $firstElement = $( this[0] ), + previousControl = $firstElement.data( Pagination.prototype.DATA_KEY ); + // if a pagination control was found for this element, either... + if( previousControl ){ + // invoke a function on the pagination object if passed a string (the function name) + if( jQuery.type( options ) === 'string' ){ + var fn = previousControl[ options ]; + if( jQuery.type( fn ) === 'function' ){ + return fn.apply( previousControl, nonOptionsArgs ); + } + + // if passed nothing, return the previously set control + } else { + return previousControl; + } + } + // if there is no control already set, return undefined + return undefined; + } + }); +})); diff -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 -r d76253bb22c4f64c73523956d119c0c5ab0a357b client/galaxy/scripts/jq-plugins/ui/peek-column-selector.js --- /dev/null +++ b/client/galaxy/scripts/jq-plugins/ui/peek-column-selector.js @@ -0,0 +1,317 @@ +// from: https://raw.githubusercontent.com/umdjs/umd/master/jqueryPlugin.js +// Uses AMD or browser globals to create a jQuery plugin. +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else { + // Browser globals + factory(jQuery); + } + +}(function ($) { + //============================================================================== + /** Column selection using the peek display as the control. + * Adds rows to the bottom of the peek with clickable areas in each cell + * to allow the user to select columns. + * Column selection can be limited to a single column or multiple. + * (Optionally) adds a left hand column of column selection prompts. + * (Optionally) allows the column headers to be clicked/renamed + * and set to some initial value. + * (Optionally) hides comment rows. + * (Optionally) allows pre-selecting and disabling certain columns for + * each row control. + * + * Construct by selecting a peek table to be used with jQuery and + * calling 'peekColumnSelector' with options. + * Options must include a 'controls' array and can include other options + * listed below. + * @example: + * $( 'pre.peek' ).peekColumnSelector({ + * columnNames : ["Chromosome", "Start", "Base", "", "", "Qual" ], + * controls : [ + * { label: 'X Column', id: 'xColumn' }, + * { label: 'Y Column', id: 'yColumn', selected: 2 }, + * { label: 'ID Column', id: 'idColumn', selected: 4, disabled: [ 1, 5 ] }, + * { label: 'Heatmap', id: 'heatmap', selected: [ 2, 4 ], disabled: [ 0, 1 ], multiselect: true, + * selectedText: 'Included', unselectedText: 'Excluded' } + * ], + * renameColumns : true, + * hideCommentRows : true, + * includePrompts : true, + * topLeftContent : 'Data sample:' + * }).on( 'peek-column-selector.change', function( ev, selection ){ + * console.info( 'new selection:', selection ); + * //{ yColumn: 2 } + * }).on( 'peek-column-selector.rename', function( ev, names ){ + * console.info( 'column names', names ); + * //[ 'Bler', 'Start', 'Base', '', '', 'Qual' ] + * }); + * + * An event is fired when column selection is changed and the event + * is passed an object in the form: { the row id : the new selection value }. + * An event is also fired when the table headers are re-named and + * is passed the new array of column names. + */ + + /** option defaults */ + var defaults = { + /** does this control allow renaming headers? */ + renameColumns : false, + /** does this control allow renaming headers? */ + columnNames : [], + /** the comment character used by the peek's datatype */ + commentChar : '#', + /** should comment rows be shown or hidden in the peek */ + hideCommentRows : false, + /** should a column of row control prompts be used */ + includePrompts : true, + /** what is the content of the top left cell (often a title) */ + topLeftContent : 'Columns:' + }, + /** class added to the pre.peek element (to allow css on just the control) */ + PEEKCONTROL_CLASS = 'peek-column-selector', + /** the string of the event fired when a control row changes */ + CHANGE_EVENT = 'peek-column-selector.change', + /** the string of the event fired when a column is renamed */ + RENAME_EVENT = 'peek-column-selector.rename', + /** class added to the control rows */ + ROW_CLASS = 'control', + /** class added to the left-hand cells that serve as row prompts */ + PROMPT_CLASS = 'control-prompt', + /** class added to selected _cells_/tds */ + SELECTED_CLASS = 'selected', + /** class added to disabled/un-clickable cells/tds */ + DISABLED_CLASS = 'disabled', + /** class added to the clickable surface within a cell to select it */ + BUTTON_CLASS = 'button', + /** class added to peek table header (th) cells to indicate they can be clicked and are renamable */ + RENAMABLE_HEADER_CLASS = 'renamable-header', + /** the data key used for each cell to store the column index ('data-...') */ + COLUMN_INDEX_DATA_KEY = 'column-index', + /** renamable header data key used to store the column name (w/o the number and dot: '1.Bler') */ + COLUMN_NAME_DATA_KEY = 'column-name'; + + //TODO: not happy with pure functional here - rows should polymorph (multi, single, etc.) + //TODO: needs clean up, move handlers to outer scope + + // ........................................................................ + /** validate the control data sent in for each row */ + function validateControl( control ){ + if( control.disabled && jQuery.type( control.disabled ) !== 'array' ){ + throw new Error( '"disabled" must be defined as an array of indeces: ' + JSON.stringify( control ) ); + } + if( control.multiselect && control.selected && jQuery.type( control.selected ) !== 'array' ){ + throw new Error( 'Mulitselect rows need an array for "selected": ' + JSON.stringify( control ) ); + } + if( !control.label || !control.id ){ + throw new Error( 'Peek controls need a label and id for each control row: ' + JSON.stringify( control ) ); + } + if( control.disabled && control.disabled.indexOf( control.selected ) !== -1 ){ + throw new Error( 'Selected column is in the list of disabled columns: ' + JSON.stringify( control ) ); + } + return control; + } + + /** build the inner control surface (i.e. button-like) */ + function buildButton( control, columnIndex ){ + return $( '<div/>' ).addClass( BUTTON_CLASS ).text( control.label ); + } + + /** build the basic (shared) cell structure */ + function buildControlCell( control, columnIndex ){ + var $td = $( '<td/>' ) + .html( buildButton( control, columnIndex ) ) + .attr( 'data-' + COLUMN_INDEX_DATA_KEY, columnIndex ); + + // disable if index in disabled array + if( control.disabled && control.disabled.indexOf( columnIndex ) !== -1 ){ + $td.addClass( DISABLED_CLASS ); + } + return $td; + } + + /** set the text of the control based on selected/un */ + function setSelectedText( $cell, control, columnIndex ){ + var $button = $cell.children( '.' + BUTTON_CLASS ); + if( $cell.hasClass( SELECTED_CLASS ) ){ + $button.html( ( control.selectedText !== undefined )?( control.selectedText ):( control.label ) ); + } else { + $button.html( ( control.unselectedText !== undefined )?( control.unselectedText ):( control.label ) ); + } + } + + /** build a cell for a row that only allows one selection */ + function buildSingleSelectCell( control, columnIndex ){ + // only one selection - selected is single index + var $cell = buildControlCell( control, columnIndex ); + if( control.selected === columnIndex ){ + $cell.addClass( SELECTED_CLASS ); + } + setSelectedText( $cell, control, columnIndex ); + + // only add the handler to non-disabled controls + if( !$cell.hasClass( DISABLED_CLASS ) ){ + $cell.click( function selectClick( ev ){ + var $cell = $( this ); + // don't re-select or fire event if already selected + if( !$cell.hasClass( SELECTED_CLASS ) ){ + // only one can be selected - remove selected on all others, add it here + var $otherSelected = $cell.parent().children( '.' + SELECTED_CLASS ).removeClass( SELECTED_CLASS ); + $otherSelected.each( function(){ + setSelectedText( $( this ), control, columnIndex ); + }); + + $cell.addClass( SELECTED_CLASS ); + setSelectedText( $cell, control, columnIndex ); + + // fire the event from the table itself, passing the id and index of selected + var eventData = {}, + key = $cell.parent().attr( 'id' ), + val = $cell.data( COLUMN_INDEX_DATA_KEY ); + eventData[ key ] = val; + $cell.parents( '.peek' ).trigger( CHANGE_EVENT, eventData ); + } + }); + } + return $cell; + } + + /** build a cell for a row that allows multiple selections */ + function buildMultiSelectCell( control, columnIndex ){ + var $cell = buildControlCell( control, columnIndex ); + // multiple selection - selected is an array + if( control.selected && control.selected.indexOf( columnIndex ) !== -1 ){ + $cell.addClass( SELECTED_CLASS ); + } + setSelectedText( $cell, control, columnIndex ); + + // only add the handler to non-disabled controls + if( !$cell.hasClass( DISABLED_CLASS ) ){ + $cell.click( function multiselectClick( ev ){ + var $cell = $( this ); + // can be more than one selected - toggle selected on this cell + $cell.toggleClass( SELECTED_CLASS ); + setSelectedText( $cell, control, columnIndex ); + var selectedColumnIndeces = $cell.parent().find( '.' + SELECTED_CLASS ).map( function( i, e ){ + return $( e ).data( COLUMN_INDEX_DATA_KEY ); + }); + // fire the event from the table itself, passing the id and index of selected + var eventData = {}, + key = $cell.parent().attr( 'id' ), + val = jQuery.makeArray( selectedColumnIndeces ); + eventData[ key ] = val; + $cell.parents( '.peek' ).trigger( CHANGE_EVENT, eventData ); + }); + } + return $cell; + } + + /** iterate over columns in peek and create a control for each */ + function buildControlCells( count, control ){ + var $cells = []; + // build a control for each column - using a build fn based on control + for( var columnIndex=0; columnIndex<count; columnIndex+=1 ){ + $cells.push( control.multiselect? buildMultiSelectCell( control, columnIndex ) + : buildSingleSelectCell( control, columnIndex ) ); + } + return $cells; + } + + /** build a row of controls for the peek */ + function buildControlRow( cellCount, control, includePrompts ){ + var $controlRow = $( '<tr/>' ).attr( 'id', control.id ).addClass( ROW_CLASS ); + if( includePrompts ){ + var $promptCell = $( '<td/>' ).addClass( PROMPT_CLASS ).text( control.label + ':' ); + $controlRow.append( $promptCell ); + } + $controlRow.append( buildControlCells( cellCount, control ) ); + return $controlRow; + } + + // ........................................................................ + /** add to the peek, using options for configuration, return the peek */ + function peekColumnSelector( options ){ + options = jQuery.extend( true, {}, defaults, options ); + + var $peek = $( this ).addClass( PEEKCONTROL_CLASS ), + $peektable = $peek.find( 'table' ), + // get the size of the tables - width and height, number of comment rows + columnCount = $peektable.find( 'th' ).size(), + rowCount = $peektable.find( 'tr' ).size(), + // get the rows containing text starting with the comment char (also make them grey) + $commentRows = $peektable.find( 'td[colspan]' ).map( function( e, i ){ + var $this = $( this ); + if( $this.text() && $this.text().match( new RegExp( '^' + options.commentChar ) ) ){ + return $( this ).css( 'color', 'grey' ).parent().get(0); + } + return null; + }); + + // should comment rows in the peek be hidden? + if( options.hideCommentRows ){ + $commentRows.hide(); + rowCount -= $commentRows.size(); + } + //console.debug( 'rowCount:', rowCount, 'columnCount:', columnCount, '$commentRows:', $commentRows ); + + // should a first column of control prompts be added? + if( options.includePrompts ){ + var $topLeft = $( '<th/>' ).addClass( 'top-left' ).text( options.topLeftContent ) + .attr( 'rowspan', rowCount ); + $peektable.find( 'tr' ).first().prepend( $topLeft ); + } + + // save either the options column name or the parsed text of each column header in html5 data attr and text + var $headers = $peektable.find( 'th:not(.top-left)' ).each( function( i, e ){ + var $this = $( this ), + // can be '1.name' or '1' + text = $this.text().replace( /^\d+\.*/, '' ), + name = options.columnNames[ i ] || text; + $this.attr( 'data-' + COLUMN_NAME_DATA_KEY, name ) + .text( ( i + 1 ) + (( name )?( '.' + name ):( '' )) ); + }); + + // allow renaming of columns when the header is clicked + if( options.renameColumns ){ + $headers.addClass( RENAMABLE_HEADER_CLASS ) + .click( function renameColumn(){ + // prompt for new name + var $this = $( this ), + index = $this.index() + ( options.includePrompts? 0: 1 ), + prevName = $this.data( COLUMN_NAME_DATA_KEY ), + newColumnName = prompt( 'New column name:', prevName ); + if( newColumnName !== null && newColumnName !== prevName ){ + // set the new text and data + $this.text( index + ( newColumnName?( '.' + newColumnName ):'' ) ) + .data( COLUMN_NAME_DATA_KEY, newColumnName ) + .attr( 'data-', COLUMN_NAME_DATA_KEY, newColumnName ); + // fire event for new column names + var columnNames = jQuery.makeArray( + $this.parent().children( 'th:not(.top-left)' ).map( function(){ + return $( this ).data( COLUMN_NAME_DATA_KEY ); + })); + $this.parents( '.peek' ).trigger( RENAME_EVENT, columnNames ); + } + }); + } + + // build a row for each control + options.controls.forEach( function( control, i ){ + validateControl( control ); + var $controlRow = buildControlRow( columnCount, control, options.includePrompts ); + $peektable.find( 'tbody' ).append( $controlRow ); + }); + return this; + } + + // ........................................................................ + // as jq plugin + jQuery.fn.extend({ + peekColumnSelector : function $peekColumnSelector( options ){ + return this.map( function(){ + return peekColumnSelector.call( this, options ); + }); + } + }); +})); diff -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 -r d76253bb22c4f64c73523956d119c0c5ab0a357b client/galaxy/scripts/mvc/history/multi-panel.js --- a/client/galaxy/scripts/mvc/history/multi-panel.js +++ b/client/galaxy/scripts/mvc/history/multi-panel.js @@ -2,7 +2,8 @@ "mvc/history/history-model", "mvc/history/history-panel-edit", "mvc/base-mvc", - "utils/ajax-queue" + "utils/ajax-queue", + "jq-plugins/ui/mode-button" ], function( HISTORY_MODEL, HPANEL_EDIT, baseMVC, ajaxQueue ){ window.HISTORY_MODEL = HISTORY_MODEL; //============================================================================== diff -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 -r d76253bb22c4f64c73523956d119c0c5ab0a357b client/galaxy/scripts/mvc/library/library-folderlist-view.js --- a/client/galaxy/scripts/mvc/library/library-folderlist-view.js +++ b/client/galaxy/scripts/mvc/library/library-folderlist-view.js @@ -15,322 +15,359 @@ ) { var FolderListView = Backbone.View.extend({ - el : '#folder_items_element', - defaults: { - 'include_deleted' : false - }, - // progress percentage - progress: 0, - // progress rate per one item - progressStep: 1, - // self modal - modal : null, + el : '#folder_items_element', + // progress percentage + progress: 0, + // progress rate per one item + progressStep: 1, - folderContainer: null, + folderContainer: null, - sort: 'asc', + sort: 'asc', - events: { - 'click #select-all-checkboxes' : 'selectAll', - 'click .dataset_row' : 'selectClickedRow', - 'click .sort-folder-link' : 'sortColumnClicked' - }, + events: { + 'click #select-all-checkboxes' : 'selectAll', + 'click .dataset_row' : 'selectClickedRow', + 'click .sort-folder-link' : 'sortColumnClicked' + }, - // cache of rendered views - rowViews: {}, - - initialize : function(options){ - this.options = _.defaults(this.options || {}, options); - this.fetchFolder(); - }, + collection: null, - fetchFolder: function(options){ - var options = options || {}; - this.options.include_deleted = options.include_deleted; - var that = this; + defaults: { + include_deleted: false, + page_count: null, + show_page: null + }, - this.collection = new mod_library_model.Folder(); + /** + * Initialize and fetch the folder from the server. + * @param {object} options an object with options + */ + initialize : function( options ){ + this.options = _.defaults( this.options || {}, this.defaults, options ); + this.modal = null; + // map of folder item ids to item views = cache + this.rowViews = {}; - // start to listen if someone modifies collection - this.listenTo(this.collection, 'add', this.renderOne); - this.listenTo(this.collection, 'remove', this.removeOne); + // create a collection of folder items for this view + this.collection = new mod_library_model.Folder(); - this.folderContainer = new mod_library_model.FolderContainer({id: this.options.id}); - this.folderContainer.url = this.folderContainer.attributes.urlRoot + this.options.id + '/contents'; - if (this.options.include_deleted){ - this.folderContainer.url = this.folderContainer.url + '?include_deleted=true'; - } - this.folderContainer.fetch({ - success: function(folder_container) { - that.folder_container = folder_container; - that.render(); - that.addAll(folder_container.get('folder').models); - if (that.options.dataset_id){ - row = _.findWhere(that.rowViews, {id: that.options.dataset_id}); - if (row) { + // start to listen if someone modifies the collection + this.listenTo( this.collection, 'add', this.renderOne ); + this.listenTo( this.collection, 'remove', this.removeOne ); + + this.fetchFolder(); + }, + + fetchFolder: function( options ){ + var options = options || {}; + this.options.include_deleted = options.include_deleted; + var that = this; + + this.folderContainer = new mod_library_model.FolderContainer( { id: this.options.id } ); + this.folderContainer.url = this.folderContainer.attributes.urlRoot + this.options.id + '/contents'; + + if ( this.options.include_deleted ){ + this.folderContainer.url = this.folderContainer.url + '?include_deleted=true'; + } + this.folderContainer.fetch({ + success: function( folder_container ) { + that.folder_container = folder_container; + that.render(); + }, + error: function( model, response ){ + if ( typeof response.responseJSON !== "undefined" ){ + mod_toastr.error( response.responseJSON.err_msg + ' Click this to go back.', '', { onclick: function() { Galaxy.libraries.library_router.back(); } } ); + } else { + mod_toastr.error( 'An error ocurred. Click this to go back.', '', { onclick: function() { Galaxy.libraries.library_router.back(); } } ); + } + } + }); + }, + + render: function ( options ){ + this.options = _.extend( this.options, options ); + var template = this.templateFolder(); + $(".tooltip").hide(); + + // find the upper id in the full path + var path = this.folderContainer.attributes.metadata.full_path; + var upper_folder_id; + if ( path.length === 1 ){ // the library is above us + upper_folder_id = 0; + } else { + upper_folder_id = path[ path.length-2 ][ 0 ]; + } + + this.$el.html( template( { + path: this.folderContainer.attributes.metadata.full_path, + parent_library_id: this.folderContainer.attributes.metadata.parent_library_id, + id: this.options.id, + upper_folder_id: upper_folder_id, + order: this.sort + } ) ); + + // when dataset_id is present render its details too + if ( this.options.dataset_id ){ + row = _.findWhere( that.rowViews, { id: this.options.dataset_id } ); + if ( row ) { row.showDatasetDetails(); } else { - mod_toastr.error('Dataset not found. Showing folder instead.'); + mod_toastr.error( 'Requested dataset not found. Showing folder instead.' ); } + } else { + if ( this.options.show_page === null || this.options.show_page < 1 ){ + this.options.show_page = 1; + } + this.paginate(); + } + $("#center [data-toggle]").tooltip(); + $("#center").css('overflow','auto'); + }, + + paginate: function( options ){ + this.options = _.extend( this.options, options ); + + if ( this.options.show_page === null || this.options.show_page < 1 ){ + this.options.show_page = 1; + } + this.options.total_items_count = this.folder_container.get( 'folder' ).models.length; + this.options.page_count = Math.ceil( this.options.total_items_count / Galaxy.libraries.preferences.get( 'folder_page_size' ) ); + var page_start = ( Galaxy.libraries.preferences.get( 'folder_page_size' ) * ( this.options.show_page - 1 ) ); + var items_to_render = null; + items_to_render = this.folder_container.get( 'folder' ).models.slice( page_start, page_start + Galaxy.libraries.preferences.get( 'folder_page_size' ) ); + this.options.items_shown = items_to_render.length; + // User requests page with no items + if ( Galaxy.libraries.preferences.get( 'folder_page_size' ) * this.options.show_page > ( this.options.total_items_count + Galaxy.libraries.preferences.get( 'folder_page_size' ) ) ){ + items_to_render = []; + } + Galaxy.libraries.folderToolbarView.renderPaginator( this.options ); + this.collection.reset(); + this.addAll( items_to_render ) + }, + + /** + * Adds all given models to the collection. + * @param {array of Item or FolderAsModel} array of models that should + * be added to the view's collection. + */ + addAll: function( models ){ + _.each(models, function( model ) { + Galaxy.libraries.folderListView.collection.add( model ); + }); + $( "#center [data-toggle]" ).tooltip(); + this.checkEmptiness(); + this.postRender(); + }, + + /** + * Call this after all models are added to the collection + * to ensure that the folder toolbar will show proper options + * and that event will be bound on all subviews. + */ + postRender: function(){ + var fetched_metadata = this.folderContainer.attributes.metadata; + fetched_metadata.contains_file = typeof this.collection.findWhere({type: 'file'}) !== 'undefined'; + Galaxy.libraries.folderToolbarView.configureElements(fetched_metadata); + $('.library-row').hover(function() { + $(this).find('.show_on_hover').show(); + }, function () { + $(this).find('.show_on_hover').hide(); + }); + }, + + /** + * Iterates this view's collection and calls the render + * function for each. Also binds the hover behavior. + */ + renderAll: function(){ + var that = this; + _.each( this.collection.models.reverse(), function( model ) { + that.renderOne( model ); + }); + this.postRender(); + }, + + /** + * Creates a view for the given model and adds it to the folder view. + * @param {Item or FolderAsModel} model of the view that will be rendered + */ + renderOne: function(model){ + if (model.get('type') !== 'folder'){ + this.options.contains_file = true; + // model.set('readable_size', this.size_to_string(model.get('file_size'))); } - }, - error: function(model, response){ - if (typeof response.responseJSON !== "undefined"){ - mod_toastr.error(response.responseJSON.err_msg + ' Click this to go back.', '', {onclick: function() {Galaxy.libraries.library_router.back();}}); - } else { - mod_toastr.error('An error ocurred. Click this to go back.', '', {onclick: function() {Galaxy.libraries.library_router.back();}}); - } + model.set('folder_id', this.id); + var rowView = new mod_library_folderrow_view.FolderRowView(model); + + // save new rowView to cache + this.rowViews[model.get('id')] = rowView; + + this.$el.find('#first_folder_item').after(rowView.el); + + $('.library-row').hover(function() { + $(this).find('.show_on_hover').show(); + }, function () { + $(this).find('.show_on_hover').hide(); + }); + }, + + /** + * removes the view of the given model from the DOM + * @param {Item or FolderAsModel} model of the view that will be removed + */ + removeOne: function( model ){ + this.$el.find( '#' + model.id ).remove(); + }, + + /** Checks whether the list is empty and adds/removes the message */ + checkEmptiness : function(){ + if ((this.$el.find('.dataset_row').length === 0) && (this.$el.find('.folder_row').length === 0)){ + this.$el.find('.empty-folder-message').show(); + } else { + this.$el.find('.empty-folder-message').hide(); } - }); - }, + }, - render: function (options) { - this.options = _.defaults(this.options, options); - var template = this.templateFolder(); - $(".tooltip").hide(); + /** User clicked the table heading = he wants to sort stuff */ + sortColumnClicked : function(event){ + event.preventDefault(); + if (this.sort === 'asc'){ + this.sortFolder('name','desc'); + this.sort = 'desc'; + } else { + this.sortFolder('name','asc'); + this.sort = 'asc'; + } + this.render(); + this.renderAll(); + this.checkEmptiness(); + }, - // TODO move to server - // find the upper id in the full path - var path = this.folderContainer.attributes.metadata.full_path; - var upper_folder_id; - if (path.length === 1){ // the library is above us - upper_folder_id = 0; - } else { - upper_folder_id = path[path.length-2][0]; + /** + * Sorts the underlying collection according to the parameters received. + * Currently supports only sorting by name. + */ + sortFolder: function(sort_by, order){ + if (sort_by === 'name'){ + if (order === 'asc'){ + return this.collection.sortByNameAsc(); + } else if (order === 'desc'){ + return this.collection.sortByNameDesc(); + } + } + }, + + /** + * User clicked the checkbox in the table heading + * @param {context} event + */ + selectAll : function (event) { + var selected = event.target.checked; + that = this; + // Iterate each checkbox + $(':checkbox', '#folder_list_body').each(function() { + this.checked = selected; + $row = $(this.parentElement.parentElement); + // Change color of selected/unselected + if (selected) { + that.makeDarkRow($row); + } else { + that.makeWhiteRow($row); + } + }); + }, + + /** + * Check checkbox if user clicks on the whole row or + * on the checkbox itself + */ + selectClickedRow : function (event) { + var checkbox = ''; + var $row; + var source; + if (event.target.localName === 'input'){ + checkbox = event.target; + $row = $(event.target.parentElement.parentElement); + source = 'input'; + } else if (event.target.localName === 'td') { + checkbox = $("#" + event.target.parentElement.id).find(':checkbox')[0]; + $row = $(event.target.parentElement); + source = 'td'; + } + if (checkbox.checked){ + if (source==='td'){ + checkbox.checked = ''; + this.makeWhiteRow($row); + } else if (source==='input') { + this.makeDarkRow($row); + } + } else { + if (source==='td'){ + checkbox.checked = 'selected'; + this.makeDarkRow($row); + } else if (source==='input') { + this.makeWhiteRow($row); + } + } + }, + + makeDarkRow: function($row){ + $row.removeClass('light').addClass('dark'); + $row.find('a').removeClass('light').addClass('dark'); + $row.find('.fa-file-o').removeClass('fa-file-o').addClass('fa-file'); + }, + + makeWhiteRow: function($row){ + $row.removeClass('dark').addClass('light'); + $row.find('a').removeClass('dark').addClass('light'); + $row.find('.fa-file').removeClass('fa-file').addClass('fa-file-o'); + }, + + templateFolder : function (){ + var tmpl_array = []; + + // BREADCRUMBS + tmpl_array.push('<ol class="breadcrumb">'); + tmpl_array.push(' <li><a title="Return to the list of libraries" href="#">Libraries</a></li>'); + tmpl_array.push(' <% _.each(path, function(path_item) { %>'); + tmpl_array.push(' <% if (path_item[0] != id) { %>'); + tmpl_array.push(' <li><a title="Return to this folder" href="#/folders/<%- path_item[0] %>"><%- path_item[1] %></a></li> '); + tmpl_array.push( '<% } else { %>'); + tmpl_array.push(' <li class="active"><span title="You are in this folder"><%- path_item[1] %></span></li>'); + tmpl_array.push(' <% } %>'); + tmpl_array.push(' <% }); %>'); + tmpl_array.push('</ol>'); + + // FOLDER CONTENT + tmpl_array.push('<table data-library-id="<%- parent_library_id %>" id="folder_table" class="grid table table-condensed">'); + tmpl_array.push(' <thead>'); + tmpl_array.push(' <th class="button_heading"></th>'); + tmpl_array.push(' <th style="text-align: center; width: 20px; " title="Check to select all datasets"><input id="select-all-checkboxes" style="margin: 0;" type="checkbox"></th>'); + tmpl_array.push(' <th><a class="sort-folder-link" title="Click to reverse order" href="#">name</a><span title="Sorted alphabetically" class="fa fa-sort-alpha-<%- order %>"></span></th>'); + tmpl_array.push(' <th style="width:5%;">data type</th>'); + tmpl_array.push(' <th style="width:10%;">size</th>'); + tmpl_array.push(' <th style="width:160px;">time updated (UTC)</th>'); + tmpl_array.push(' <th style="width:10%;"></th> '); + tmpl_array.push(' </thead>'); + tmpl_array.push(' <tbody id="folder_list_body">'); + tmpl_array.push(' <tr id="first_folder_item">'); + tmpl_array.push(' <td><a href="#<% if (upper_folder_id !== 0){ print("folders/" + upper_folder_id)} %>" title="Go to parent folder" class="btn_open_folder btn btn-default btn-xs">..<a></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' <td></td>'); + tmpl_array.push(' </tr>'); + + tmpl_array.push(' </tbody>'); + tmpl_array.push('</table>'); + tmpl_array.push('<div class="empty-folder-message" style="display:none;">This folder is either empty or you do not have proper access permissions to see the contents. If you expected something to show up please consult the <a href="https://wiki.galaxyproject.org/Admin/DataLibraries/LibrarySecurity" target="_blank">library security wikipage</a> or visit the <a href="https://biostar.usegalaxy.org/" target="_blank">Galaxy support site</a>.</div>'); + + return _.template(tmpl_array.join('')); } - - this.$el.html(template({ path: this.folderContainer.attributes.metadata.full_path, parent_library_id: this.folderContainer.attributes.metadata.parent_library_id, id: this.options.id, upper_folder_id: upper_folder_id, order: this.sort})); - - // initialize the library tooltips - $("#center [data-toggle]").tooltip(); - //hack to show scrollbars - $("#center").css('overflow','auto'); - }, - - /** - * Call this after all models are added to the collection - * to ensure that the folder toolbar will show proper options - * and that event will be bound on all subviews. - */ - postRender: function(){ - var fetched_metadata = this.folderContainer.attributes.metadata; - fetched_metadata.contains_file = typeof this.collection.findWhere({type: 'file'}) !== 'undefined'; - Galaxy.libraries.folderToolbarView.configureElements(fetched_metadata); - $('.library-row').hover(function() { - $(this).find('.show_on_hover').show(); - }, function () { - $(this).find('.show_on_hover').hide(); - }); - }, - - /** - * Adds all given models to the collection. - * @param {array of Item or FolderAsModel} array of models that should - * be added to the view's collection. - */ - addAll: function(models){ - _.each(models.reverse(), function(model) { - Galaxy.libraries.folderListView.collection.add(model); - }); - - $("#center [data-toggle]").tooltip(); - this.checkEmptiness(); - - this.postRender(); - }, - - /** - * Iterates this view's collection and calls the render - * function for each. Also binds the hover behavior. - */ - renderAll: function(){ - var that = this; - _.each(this.collection.models.reverse(), function(model) { - that.renderOne(model); - }); - this.postRender(); - }, - - /** - * Creates a view for the given model and adds it to the folder view. - * @param {Item or FolderAsModel} model of the view that will be rendered - */ - renderOne: function(model){ - if (model.get('type') !== 'folder'){ - this.options.contains_file = true; - // model.set('readable_size', this.size_to_string(model.get('file_size'))); - } - model.set('folder_id', this.id); - var rowView = new mod_library_folderrow_view.FolderRowView(model); - - // save new rowView to cache - this.rowViews[model.get('id')] = rowView; - - this.$el.find('#first_folder_item').after(rowView.el); - - $('.library-row').hover(function() { - $(this).find('.show_on_hover').show(); - }, function () { - $(this).find('.show_on_hover').hide(); - }); - }, - - /** - * removes the view of the given model from the DOM - * @param {Item or FolderAsModel} model of the view that will be removed - */ - removeOne: function(model){ - this.$el.find('#' + model.id).remove(); - }, - - /** Checks whether the list is empty and adds/removes the message */ - checkEmptiness : function(){ - if ((this.$el.find('.dataset_row').length === 0) && (this.$el.find('.folder_row').length === 0)){ - this.$el.find('.empty-folder-message').show(); - } else { - this.$el.find('.empty-folder-message').hide(); - } - }, - - /** User clicked the table heading = he wants to sort stuff */ - sortColumnClicked : function(event){ - event.preventDefault(); - if (this.sort === 'asc'){ - this.sortFolder('name','desc'); - this.sort = 'desc'; - } else { - this.sortFolder('name','asc'); - this.sort = 'asc'; - } - this.render(); - this.renderAll(); - this.checkEmptiness(); - }, - - /** - * Sorts the underlying collection according to the parameters received. - * Currently supports only sorting by name. - */ - sortFolder: function(sort_by, order){ - if (sort_by === 'name'){ - if (order === 'asc'){ - return this.collection.sortByNameAsc(); - } else if (order === 'desc'){ - return this.collection.sortByNameDesc(); - } - } - }, - - /** - * User clicked the checkbox in the table heading - * @param {context} event - */ - selectAll : function (event) { - var selected = event.target.checked; - that = this; - // Iterate each checkbox - $(':checkbox', '#folder_list_body').each(function() { - this.checked = selected; - $row = $(this.parentElement.parentElement); - // Change color of selected/unselected - if (selected) { - that.makeDarkRow($row); - } else { - that.makeWhiteRow($row); - } - }); - }, - - /** - * Check checkbox if user clicks on the whole row or - * on the checkbox itself - */ - selectClickedRow : function (event) { - var checkbox = ''; - var $row; - var source; - if (event.target.localName === 'input'){ - checkbox = event.target; - $row = $(event.target.parentElement.parentElement); - source = 'input'; - } else if (event.target.localName === 'td') { - checkbox = $("#" + event.target.parentElement.id).find(':checkbox')[0]; - $row = $(event.target.parentElement); - source = 'td'; - } - if (checkbox.checked){ - if (source==='td'){ - checkbox.checked = ''; - this.makeWhiteRow($row); - } else if (source==='input') { - this.makeDarkRow($row); - } - } else { - if (source==='td'){ - checkbox.checked = 'selected'; - this.makeDarkRow($row); - } else if (source==='input') { - this.makeWhiteRow($row); - } - } - }, - - makeDarkRow: function($row){ - $row.removeClass('light').addClass('dark'); - $row.find('a').removeClass('light').addClass('dark'); - $row.find('.fa-file-o').removeClass('fa-file-o').addClass('fa-file'); - }, - - makeWhiteRow: function($row){ - $row.removeClass('dark').addClass('light'); - $row.find('a').removeClass('dark').addClass('light'); - $row.find('.fa-file').removeClass('fa-file').addClass('fa-file-o'); - }, - - templateFolder : function (){ - var tmpl_array = []; - - // BREADCRUMBS - tmpl_array.push('<ol class="breadcrumb">'); - tmpl_array.push(' <li><a title="Return to the list of libraries" href="#">Libraries</a></li>'); - tmpl_array.push(' <% _.each(path, function(path_item) { %>'); - tmpl_array.push(' <% if (path_item[0] != id) { %>'); - tmpl_array.push(' <li><a title="Return to this folder" href="#/folders/<%- path_item[0] %>"><%- path_item[1] %></a></li> '); - tmpl_array.push( '<% } else { %>'); - tmpl_array.push(' <li class="active"><span title="You are in this folder"><%- path_item[1] %></span></li>'); - tmpl_array.push(' <% } %>'); - tmpl_array.push(' <% }); %>'); - tmpl_array.push('</ol>'); - - // FOLDER CONTENT - tmpl_array.push('<table data-library-id="<%- parent_library_id %>" id="folder_table" class="grid table table-condensed">'); - tmpl_array.push(' <thead>'); - tmpl_array.push(' <th class="button_heading"></th>'); - tmpl_array.push(' <th style="text-align: center; width: 20px; " title="Check to select all datasets"><input id="select-all-checkboxes" style="margin: 0;" type="checkbox"></th>'); - tmpl_array.push(' <th><a class="sort-folder-link" title="Click to reverse order" href="#">name</a><span title="Sorted alphabetically" class="fa fa-sort-alpha-<%- order %>"></span></th>'); - tmpl_array.push(' <th style="width:5%;">data type</th>'); - tmpl_array.push(' <th style="width:10%;">size</th>'); - tmpl_array.push(' <th style="width:160px;">time updated (UTC)</th>'); - tmpl_array.push(' <th style="width:10%;"></th> '); - tmpl_array.push(' </thead>'); - tmpl_array.push(' <tbody id="folder_list_body">'); - tmpl_array.push(' <tr id="first_folder_item">'); - tmpl_array.push(' <td><a href="#<% if (upper_folder_id !== 0){ print("folders/" + upper_folder_id)} %>" title="Go to parent folder" class="btn_open_folder btn btn-default btn-xs">..<a></td>'); - tmpl_array.push(' <td></td>'); - tmpl_array.push(' <td></td>'); - tmpl_array.push(' <td></td>'); - tmpl_array.push(' <td></td>'); - tmpl_array.push(' <td></td>'); - tmpl_array.push(' <td></td>'); - tmpl_array.push(' </tr>'); - - tmpl_array.push(' </tbody>'); - tmpl_array.push('</table>'); - tmpl_array.push('<div class="empty-folder-message" style="display:none;">This folder is either empty or you do not have proper access permissions to see the contents. If you expected something to show up please consult the <a href="https://wiki.galaxyproject.org/Admin/DataLibraries/LibrarySecurity" target="_blank">library security wikipage</a> or visit the <a href="https://biostar.usegalaxy.org/" target="_blank">Galaxy support site</a>.</div>'); - - return _.template(tmpl_array.join('')); - } - + }); return { diff -r 5edaafeecfed448ea259adaacf3a4ce6e6da15d1 -r d76253bb22c4f64c73523956d119c0c5ab0a357b client/galaxy/scripts/mvc/library/library-foldertoolbar-view.js --- a/client/galaxy/scripts/mvc/library/library-foldertoolbar-view.js +++ b/client/galaxy/scripts/mvc/library/library-foldertoolbar-view.js @@ -20,7 +20,9 @@ 'click #toolbtn_bulk_import' : 'modalBulkImport', 'click #include_deleted_datasets_chk' : 'checkIncludeDeleted', 'click #toolbtn_show_libinfo' : 'showLibInfo', - 'click #toolbtn_bulk_delete' : 'deleteSelectedDatasets' + 'click #toolbtn_bulk_delete' : 'deleteSelectedDatasets', + 'click #page_size_prompt' : 'showPageSizePrompt' + }, defaults: { @@ -90,6 +92,22 @@ this.$el.html(toolbar_template(template_defaults)); }, + /** + * Called from FolderListView when needed. + * @param {object} options common options + */ + renderPaginator: function( options ){ + this.options = _.extend( this.options, options ); + var paginator_template = this.templatePaginator(); + this.$el.find( '#folder_paginator' ).html( paginator_template({ + id: this.options.id, + show_page: parseInt( this.options.show_page ), + page_count: parseInt( this.options.page_count ), + total_items_count: this.options.total_items_count, + items_shown: this.options.items_shown + })); + }, + configureElements: function(options){ this.options = _.extend(this.options, options); @@ -833,11 +851,11 @@ var popped_item = lddas_set.pop(); if ( typeof popped_item === "undefined" ) { if ( this.options.chain_call_control.failed_number === 0 ){ - mod_toastr.success( 'Selected datasets deleted' ); + mod_toastr.success( 'Selected datasets were deleted.' ); } else if ( this.options.chain_call_control.failed_number === this.options.chain_call_control.total_number ){ - mod_toastr.error( 'There was an error and no datasets were deleted.' ); + mod_toastr.error( 'There was an error and no datasets were deleted. Please make sure you have sufficient permissions.' ); } else if ( this.options.chain_call_control.failed_number < this.options.chain_call_control.total_number ){ - mod_toastr.warning( 'Some of the datasets could not be deleted' ); + mod_toastr.warning( 'Some of the datasets could not be deleted. Please make sure you have sufficient permissions.' ); } Galaxy.modal.hide(); return this.deleted_lddas; @@ -976,6 +994,14 @@ } }, + showPageSizePrompt: function(){ + var folder_page_size = prompt( 'How many items per page do you want to see?', Galaxy.libraries.preferences.get( 'folder_page_size' ) ); + if ( ( folder_page_size != null ) && ( folder_page_size == parseInt( folder_page_size ) ) ) { + Galaxy.libraries.preferences.set( { 'folder_page_size': parseInt( folder_page_size ) } ); + Galaxy.libraries.folderListView.render( { id: this.options.id, show_page: 1 } ); + } + }, + templateToolBar: function(){ tmpl_array = []; @@ -1016,7 +1042,6 @@ tmpl_array.push(' <button style="display:none;" data-toggle="tooltip" data-placement="top" title="Add Datasets to Current Folder" id="toolbtn_add_files" class="btn btn-default toolbtn_add_files primary-button add-library-items" type="button"><span class="fa fa-plus"></span><span class="fa fa-file"></span></span></button>'); tmpl_array.push('<% } %>'); - tmpl_array.push(' <button data-toggle="tooltip" data-placement="top" title="Import selected datasets into history" id="toolbtn_bulk_import" class="primary-button dataset-manipulation" style="margin-left: 0.5em; display:none;" type="button"><span class="fa fa-book"></span> to History</button>'); tmpl_array.push(' <div id="toolbtn_dl" class="btn-group dataset-manipulation" style="margin-left: 0.5em; display:none; ">'); tmpl_array.push(' <button title="Download selected datasets as archive" id="drop_toggle" type="button" class="primary-button dropdown-toggle" data-toggle="dropdown">'); @@ -1032,6 +1057,10 @@ tmpl_array.push(' <button data-id="<%- id %>" data-toggle="tooltip" data-placement="top" title="Show library information" id="toolbtn_show_libinfo" class="primary-button" style="margin-left: 0.5em;" type="button"><span class="fa fa-info-circle"></span> Library Info</button>'); tmpl_array.push(' <span class="help-button" data-toggle="tooltip" data-placement="top" title="Visit Libraries Wiki"><a href="https://wiki.galaxyproject.org/DataLibraries/screen/FolderContents" target="_blank"><button class="primary-button" type="button"><span class="fa fa-question-circle"></span> Help</button></a></span>'); + tmpl_array.push(' <span id="folder_paginator" class="library-paginator">'); + // paginator will append here + tmpl_array.push(' </span>'); + tmpl_array.push(' </div>'); // TOOLBAR END tmpl_array.push(' <div id="folder_items_element">'); @@ -1239,7 +1268,41 @@ tmpl_array.push('</ul>'); return _.template(tmpl_array.join('')); - } + }, + + templatePaginator: function(){ + tmpl_array = []; + + tmpl_array.push(' <ul class="pagination pagination-sm">'); + tmpl_array.push(' <% if ( ( show_page - 1 ) > 0 ) { %>'); + tmpl_array.push(' <% if ( ( show_page - 1 ) > page_count ) { %>'); // we are on higher page than total page count + tmpl_array.push(' <li><a href="#folders/<%= id %>/page/1"><span class="fa fa-angle-double-left"></span></a></li>'); + tmpl_array.push(' <li class="disabled"><a href="#folders/<%= id %>/page/<% print( show_page ) %>"><% print( show_page - 1 ) %></a></li>'); + tmpl_array.push(' <% } else { %>'); + tmpl_array.push(' <li><a href="#folders/<%= id %>/page/1"><span class="fa fa-angle-double-left"></span></a></li>'); + tmpl_array.push(' <li><a href="#folders/<%= id %>/page/<% print( show_page - 1 ) %>"><% print( show_page - 1 ) %></a></li>'); + tmpl_array.push(' <% } %>'); + tmpl_array.push(' <% } else { %>'); // we are on the first page + tmpl_array.push(' <li class="disabled"><a href="#folders/<%= id %>/page/1"><span class="fa fa-angle-double-left"></span></a></li>'); + tmpl_array.push(' <li class="disabled"><a href="#folders/<%= id %>/page/<% print( show_page ) %>"><% print( show_page - 1 ) %></a></li>'); + tmpl_array.push(' <% } %>'); + tmpl_array.push(' <li class="active">'); + tmpl_array.push(' <a href="#folders/<%= id %>/page/<% print( show_page ) %>"><% print( show_page ) %></a>'); + tmpl_array.push(' </li>'); + tmpl_array.push(' <% if ( ( show_page ) < page_count ) { %>'); + tmpl_array.push(' <li><a href="#folders/<%= id %>/page/<% print( show_page + 1 ) %>"><% print( show_page + 1 ) %></a></li>'); + tmpl_array.push(' <li><a href="#folders/<%= id %>/page/<% print( page_count ) %>"><span class="fa fa-angle-double-right"></span></a></li>'); + tmpl_array.push(' <% } else { %>'); + tmpl_array.push(' <li class="disabled"><a href="#folders/<%= id %>/page/<% print( show_page ) %>"><% print( show_page + 1 ) %></a></li>'); + tmpl_array.push(' <li class="disabled"><a href="#folders/<%= id %>/page/<% print( page_count ) %>"><span class="fa fa-angle-double-right"></span></a></li>'); + tmpl_array.push(' <% } %>'); + tmpl_array.push(' </ul>'); + tmpl_array.push(' <span>'); + tmpl_array.push(' showing <a data-toggle="tooltip" data-placement="top" title="Click to change the number of items on page" id="page_size_prompt"><%- items_shown %></a> of <%- total_items_count %> items'); + tmpl_array.push(' </span>'); + + return _.template(tmpl_array.join('')); + }, }); This diff is so big that we needed to truncate the remainder. https://bitbucket.org/galaxy/galaxy-central/commits/a0bbd59cb420/ Changeset: a0bbd59cb420 User: abretaud Date: 2014-12-11 16:02:05+00:00 Summary: fix method names and api doc Affected #: 2 files diff -r d76253bb22c4f64c73523956d119c0c5ab0a357b -r a0bbd59cb420442c2fb518540fd2fdfbb2b2da0a lib/galaxy/tools/data/__init__.py --- a/lib/galaxy/tools/data/__init__.py +++ b/lib/galaxy/tools/data/__init__.py @@ -211,11 +211,11 @@ self.add_entry( entry, allow_duplicates=allow_duplicates, persist=persist, persist_on_error=persist_on_error, entry_source=entry_source, **kwd ) return self._loaded_content_version - def _remove_entry(self, values, **kwd): + def _remove_entry(self, values): raise NotImplementedError( "Abstract method" ) - def remove_entry(self, values, **kwd): - self._remove_entry_and_reload( values,**kwd ) + def remove_entry(self, values): + self._remove_entry( values ) return self._update_version() def is_current_version( self, other_version ): @@ -513,7 +513,7 @@ data_table_fh.write( "%s\n" % ( self.separator.join( fields ) ) ) return not is_error - def _remove_entry_and_reload( self, values): + def _remove_entry( self, values): # update every file for filename in self.filenames: diff -r d76253bb22c4f64c73523956d119c0c5ab0a357b -r a0bbd59cb420442c2fb518540fd2fdfbb2b2da0a lib/galaxy/webapps/galaxy/api/tool_data.py --- a/lib/galaxy/webapps/galaxy/api/tool_data.py +++ b/lib/galaxy/webapps/galaxy/api/tool_data.py @@ -26,10 +26,10 @@ def delete( self, trans, id, **kwd ): """ DELETE /api/tool_data/{id} - Removes a role from a group + Removes an item from a data table :type id: str - :param id: the encoded id of the history to delete + :param id: the id of the data table containing the item to delete :type kwd: dict :param kwd: (required) dictionary structure containing: https://bitbucket.org/galaxy/galaxy-central/commits/6363c839528c/ Changeset: 6363c839528c User: jmchilton Date: 2014-12-15 05:17:31+00:00 Summary: Merged in abretaud/galaxy-central (pull request #577) Add an API to remove items from tool data tables Affected #: 2 files diff -r ac537b0a4167cbd14e0cb93175cc85c096275697 -r 6363c839528ca8bbbbc60c1b3784c788468d9445 lib/galaxy/tools/data/__init__.py --- a/lib/galaxy/tools/data/__init__.py +++ b/lib/galaxy/tools/data/__init__.py @@ -211,6 +211,13 @@ self.add_entry( entry, allow_duplicates=allow_duplicates, persist=persist, persist_on_error=persist_on_error, entry_source=entry_source, **kwd ) return self._loaded_content_version + def _remove_entry(self, values): + raise NotImplementedError( "Abstract method" ) + + def remove_entry(self, values): + self._remove_entry( values ) + return self._update_version() + def is_current_version( self, other_version ): return self._loaded_content_version == other_version @@ -506,6 +513,42 @@ data_table_fh.write( "%s\n" % ( self.separator.join( fields ) ) ) return not is_error + def _remove_entry( self, values): + + # update every file + for filename in self.filenames: + + if os.path.exists( filename ): + values = self._replace_field_separators( values ) + self.filter_file_fields( filename, values ) + else: + log.warn( "Cannot find index file '%s' for tool data table '%s'" % ( filename, self.name ) ) + + self.reload_from_files() + + def filter_file_fields( self, loc_file, values ): + """ + Reads separated lines from file and print back only the lines that pass a filter. + """ + separator_char = (lambda c: '<TAB>' if c == '\t' else c)(self.separator) + + with open(loc_file) as reader: + rval = "" + for i, line in enumerate( reader ): + if line.lstrip().startswith( self.comment_char ): + rval += line + else: + line_s = line.rstrip( "\n\r" ) + if line_s: + fields = line_s.split( self.separator ) + if fields != values: + rval += line + + with open(loc_file, 'wb') as writer: + writer.write(rval) + + return rval + def _replace_field_separators( self, fields, separator=None, replace=None, comment_char=None ): #make sure none of the fields contain separator #make sure separator replace is different from comment_char, diff -r ac537b0a4167cbd14e0cb93175cc85c096275697 -r 6363c839528ca8bbbbc60c1b3784c788468d9445 lib/galaxy/webapps/galaxy/api/tool_data.py --- a/lib/galaxy/webapps/galaxy/api/tool_data.py +++ b/lib/galaxy/webapps/galaxy/api/tool_data.py @@ -20,3 +20,44 @@ @web.expose_api def show( self, trans, id, **kwds ): return trans.app.tool_data_tables.data_tables[id].to_dict(view='element') + + @web.require_admin + @web.expose_api + def delete( self, trans, id, **kwd ): + """ + DELETE /api/tool_data/{id} + Removes an item from a data table + + :type id: str + :param id: the id of the data table containing the item to delete + :type kwd: dict + :param kwd: (required) dictionary structure containing: + + * payload: a dictionary itself containing: + * values: <TAB> separated list of column contents, there must be a value for all the columns of the data table + """ + decoded_tool_data_id = id + + try: + data_table = trans.app.tool_data_tables.data_tables.get(decoded_tool_data_id) + except: + data_table = None + if not data_table: + trans.response.status = 400 + return "Invalid data table id ( %s ) specified." % str( decoded_tool_data_id ) + + values = None + if kwd.get( 'payload', None ): + values = kwd['payload'].get( 'values', '' ) + + if not values: + trans.response.status = 400 + return "Invalid data table item ( %s ) specified." % str( values ) + + split_values = values.split("\t") + + if len(split_values) != len(data_table.get_column_name_list()): + trans.response.status = 400 + return "Invalid data table item ( %s ) specified. Wrong number of columns (%s given, %s required)." % ( str( values ), str(len(split_values)), str(len(data_table.get_column_name_list()))) + + return data_table.remove_entry(split_values) Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.