commit/galaxy-central: martenson: paginate library folder contents to prevent loooooong rendering when hundreds of items are in one folder
1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/20942ced453d/ Changeset: 20942ced453d Branch: next-stable User: martenson Date: 2014-12-05 22:21:57+00:00 Summary: paginate library folder contents to prevent loooooong rendering when hundreds of items are in one folder Affected #: 12 files diff -r 9ddc3efa9c3245a7dfd24680bdb816f3d4852094 -r 20942ced453dc9f31b2d238855d1b8f5da3f078b 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 9ddc3efa9c3245a7dfd24680bdb816f3d4852094 -r 20942ced453dc9f31b2d238855d1b8f5da3f078b 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 9ddc3efa9c3245a7dfd24680bdb816f3d4852094 -r 20942ced453dc9f31b2d238855d1b8f5da3f078b 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('')); + }, }); diff -r 9ddc3efa9c3245a7dfd24680bdb816f3d4852094 -r 20942ced453dc9f31b2d238855d1b8f5da3f078b client/galaxy/scripts/mvc/library/library-librarylist-view.js --- a/client/galaxy/scripts/mvc/library/library-librarylist-view.js +++ b/client/galaxy/scripts/mvc/library/library-librarylist-view.js @@ -21,20 +21,19 @@ 'click .sort-libraries-link' : 'sort_clicked' }, - /** - * Initialize and fetch the libraries from server. - * Async render afterwards. - * @param {object} options an options object - */ defaults: { page_count: null, show_page: null }, + /** + * Initialize and fetch the libraries from server. + * Async render afterwards. + * @param {object} options an object with options + */ initialize : function( options ){ this.options = _.defaults( this.options || {}, this.defaults, options ); - - var that = this; + var that = this; this.modal = null; // map of library model ids to library views = cache this.rowViews = {}; @@ -61,19 +60,10 @@ */ render: function ( options ) { this.options = _.extend( this.options, options ); - - if ( ( this.options.page_size != null ) && ( this.options.page_size == parseInt( this.options.page_size ) ) ) { - Galaxy.libraries.preferences.set( { 'library_page_size': parseInt( this.options.page_size ) } ); - } - - $( ".tooltip" ).hide(); - // this.options.show_page = this.options.show_page || 1; var template = this.templateLibraryList(); var libraries_to_render = null; var models = null; - if ( this.options.show_page === null || this.options.show_page < 1 ){ - this.options.show_page = 1; - } + $( ".tooltip" ).hide(); if ( typeof options !== 'undefined' ){ models = typeof options.models !== 'undefined' ? options.models : null; } @@ -89,6 +79,10 @@ } else { libraries_to_render = []; } + // pagination + if ( this.options.show_page === null || this.options.show_page < 1 ){ + this.options.show_page = 1; + } this.options.total_libraries_count = libraries_to_render.length var page_start = ( Galaxy.libraries.preferences.get( 'library_page_size' ) * ( this.options.show_page - 1 ) ); this.options.page_count = Math.ceil( this.options.total_libraries_count / Galaxy.libraries.preferences.get( 'library_page_size' ) ); diff -r 9ddc3efa9c3245a7dfd24680bdb816f3d4852094 -r 20942ced453dc9f31b2d238855d1b8f5da3f078b static/scripts/galaxy.library.js --- a/static/scripts/galaxy.library.js +++ b/static/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 9ddc3efa9c3245a7dfd24680bdb816f3d4852094 -r 20942ced453dc9f31b2d238855d1b8f5da3f078b static/scripts/mvc/library/library-folderlist-view.js --- a/static/scripts/mvc/library/library-folderlist-view.js +++ b/static/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 { This diff is so big that we needed to truncate the remainder. 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.
participants (1)
-
commits-noreply@bitbucket.org