diff --git a/run_functional_tests.sh b/run_functional_tests.sh
new file mode 120000
index 00000000000..5236e32d82f
--- /dev/null
+++ b/run_functional_tests.sh
@@ -0,0 +1 @@
+run_tests.sh
\ No newline at end of file
diff --git a/static/scripts/mvc/base-mvc.js b/static/scripts/mvc/base-mvc.js
index 267afd7c4cf..83780d880ae 100644
--- a/static/scripts/mvc/base-mvc.js
+++ b/static/scripts/mvc/base-mvc.js
@@ -42,6 +42,7 @@ var LoggableMixin = /** @lends LoggableMixin# */{
if( this.logger ){
var log = this.logger.log;
if( typeof this.logger.log === 'object' ){
+//TODO:! there has to be a way to get the lineno/file into this
log = Function.prototype.bind.call( this.logger.log, this.logger );
}
return log.apply( this.logger, arguments );
@@ -148,6 +149,23 @@ var SessionStorageModel = Backbone.Model.extend({
}());
+//==============================================================================
+/** Function that allows mixing of hashs into bbone MVC while showing the mixins first
+ * (before the more local class overrides/hash).
+ * Basically, a simple reversal of param order on _.defaults() - to show mixins in top of definition.
+ * @example:
+ * var NewModel = Something.extend( mixin( MyMixinA, MyMixinB, { ... myVars : ... }) );
+ *
+ * NOTE: this does not combine any hashes (like events, etc.) and you're expected to handle that
+ */
+function mixin( mixinHash1, /* mixinHash2, etc: ... variadic */ propsHash ){
+ var args = Array.prototype.slice.call( arguments, 0 ),
+ lastArg = args.pop();
+ args.unshift( lastArg );
+ return _.defaults.apply( _, args );
+}
+
+
//==============================================================================
/** A mixin for models that allow T/F/Matching to their attributes - useful when
* searching or filtering collections of models.
@@ -328,205 +346,6 @@ var HiddenUntilActivatedViewMixin = /** @lends hiddenUntilActivatedMixin# */{
};
-//==============================================================================
-/** Function that allows mixing of hashs into bbone MVC while showing the mixins first
- * (before the more local class overrides/hash).
- * Basically, a simple reversal of param order on _.defaults() - to show mixins in top of definition.
- * @example:
- * var NewModel = Something.extend( mixin( MyMixinA, MyMixinB, { ... myVars : ... }) );
- *
- * NOTE: this does not combine any hashes (like events, etc.) and you're expected to handle that
- */
-function mixin( mixinHash1, /* mixinHash2, etc: ... variadic */ propsHash ){
- var args = Array.prototype.slice.call( arguments, 0 ),
- lastArg = args.pop();
- args.unshift( lastArg );
- return _.defaults.apply( _, args );
-}
-
-//==============================================================================
-/** Return an underscore template fn from an array of strings.
- * @param {String[]} template the template strings to compile into the underscore template fn
- * @param {String} jsonNamespace an optional namespace for the json data passed in (defaults to 'model')
- * @returns {Function} the (wrapped) underscore template fn
- * The function accepts:
- *
- * The template strings can access:
- * the json/model hash using model ("<%- model.myAttr %>) using the jsonNamespace above
- * _l: the localizer function
- * view (if passed): ostensibly, the view using the template (handy for view instance vars)
- * Because they're namespaced, undefined attributes will not throw an error.
- *
- * @example:
- * templateBler : BASE_MVC.wrapTemplate([
- * '
'
- * ], 'mynamespace' )
- *
- * Meant to be called in a View's definition in order to compile only once.
- *
- */
-function wrapTemplate( template, jsonNamespace ){
- jsonNamespace = jsonNamespace || 'model';
- var templateFn = _.template( template.join( '' ) );
- return function( json, view ){
- var templateVars = { view : view || {}, _l : _l };
- templateVars[ jsonNamespace ] = json || {};
- return templateFn( templateVars );
- };
-}
-
-//==============================================================================
-/** A view which, when first rendered, shows only summary data/attributes, but
- * can be expanded to show further details (and optionally fetch those
- * details from the server).
- */
-var ExpandableView = Backbone.View.extend( LoggableMixin ).extend({
-//TODO: Although the reasoning behind them is different, this shares a lot with HiddenUntilActivated above: combine them
- //PRECONDITION: model must have method hasDetails
- //PRECONDITION: subclasses must have templates.el and templates.details
-
- initialize : function( attributes ){
- /** are the details of this view expanded/shown or not? */
- this.expanded = attributes.expanded || false;
- //this.log( '\t expanded:', this.expanded );
- this.fxSpeed = attributes.fxSpeed || this.fxSpeed;
- },
-
- // ........................................................................ render main
- /** jq fx speed */
- fxSpeed : 'fast',
-
- /** Render this content, set up ui.
- * @param {Number or String} speed the speed of the render
- */
- render : function( speed ){
- var $newRender = this._buildNewRender();
- this._setUpBehaviors( $newRender );
- this._queueNewRender( $newRender, speed );
- return this;
- },
-
- /** Build a temp div containing the new children for the view's $el.
- * If the view is already expanded, build the details as well.
- */
- _buildNewRender : function(){
- // create a new render using a skeleton template, render title buttons, render body, and set up events, etc.
- var $newRender = $( this.templates.el( this.model.toJSON(), this ) );
- if( this.expanded ){
- this.$details( $newRender ).replaceWith( this._renderDetails().show() );
- }
- return $newRender;
- },
-
- /** Fade out the old el, swap in the new contents, then fade in.
- * @param {Number or String} speed jq speed to use for rendering effects
- * @fires rendered when rendered
- */
- _queueNewRender : function( $newRender, speed ) {
- speed = ( speed === undefined )?( this.fxSpeed ):( speed );
- var view = this;
-
- $( view ).queue( 'fx', [
- function( next ){ this.$el.fadeOut( speed, next ); },
- function( next ){
- view._swapNewRender( $newRender );
- next();
- },
- function( next ){ this.$el.fadeIn( speed, next ); },
- function( next ){
- this.trigger( 'rendered', view );
- next();
- }
- ]);
- },
-
- /** empty out the current el, move the $newRender's children in */
- _swapNewRender : function( $newRender ){
- return this.$el.empty().attr( 'class', this.className ).append( $newRender.children() );
- },
-
- /** set up js behaviors, event handlers for elements within the given container
- * @param {jQuery} $container jq object that contains the elements to process (defaults to this.$el)
- */
- _setUpBehaviors : function( $where ){
- $where = $where || this.$el;
- // set up canned behavior on children (bootstrap, popupmenus, editable_text, etc.)
- //make_popup_menus( $where );
- $where.find( '[title]' ).tooltip({ placement : 'bottom' });
- },
-
- // ......................................................................... details
- /** shortcut to details DOM (as jQ) */
- $details : function( $where ){
- $where = $where || this.$el;
- return $where.find( '.details' );
- },
-
- /** build the DOM for the details and set up behaviors on it */
- _renderDetails : function(){
- var $newDetails = $( this.templates.details( this.model.toJSON(), this ) );
- this._setUpBehaviors( $newDetails );
- return $newDetails;
- },
-
- // ......................................................................... expansion/details
- /** Show or hide the details
- * @param {Boolean} expand if true, expand; if false, collapse
- */
- toggleExpanded : function( expand ){
- expand = ( expand === undefined )?( !this.expanded ):( expand );
- if( expand ){
- this.expand();
- } else {
- this.collapse();
- }
- return this;
- },
-
- /** Render and show the full, detailed body of this view including extra data and controls.
- * note: if the model does not have detailed data, fetch that data before showing the body
- * @fires expanded when a body has been expanded
- */
- expand : function(){
- var view = this;
- return view._fetchModelDetails()
- .always(function(){
- var $newDetails = view._renderDetails();
- view.$details().replaceWith( $newDetails );
- // needs to be set after the above or the slide will not show
- view.expanded = true;
- $newDetails.slideDown( view.fxSpeed, function(){
- view.trigger( 'expanded', view );
- });
- });
- },
-
- /** Check for model details and, if none, fetch them.
- * @returns {jQuery.promise} the model.fetch.xhr if details are being fetched, an empty promise if not
- */
- _fetchModelDetails : function(){
- if( !this.model.hasDetails() ){
- return this.model.fetch();
- }
- return jQuery.when();
- },
-
- /** Hide the body/details of an HDA.
- * @fires collapsed when a body has been collapsed
- */
- collapse : function(){
- var view = this;
- view.expanded = false;
- this.$details().slideUp( view.fxSpeed, function(){
- view.trigger( 'collapsed', view );
- });
- }
-
-});
-
-
//==============================================================================
/** Mixin for views that can be dragged and dropped
* Allows for the drag behavior to be turned on/off, setting/removing jQuery event
@@ -711,186 +530,48 @@ var SelectableViewMixin = {
//==============================================================================
-/** A view that is displayed in some larger list/grid/collection.
- * Inherits from Expandable, Selectable, Draggable.
- * The DOM contains warnings, a title bar, and a series of primary action controls.
- * Primary actions are meant to be easily accessible item functions (such as delete)
- * that are rendered in the title bar.
+/** Return an underscore template fn from an array of strings.
+ * @param {String[]} template the template strings to compile into the underscore template fn
+ * @param {String} jsonNamespace an optional namespace for the json data passed in (defaults to 'model')
+ * @returns {Function} the (wrapped) underscore template fn
+ * The function accepts:
*
- * Details are rendered when the user clicks the title bar or presses enter/space when
- * the title bar is in focus.
+ * The template strings can access:
+ * the json/model hash using model ("<%- model.myAttr %>) using the jsonNamespace above
+ * _l: the localizer function
+ * view (if passed): ostensibly, the view using the template (handy for view instance vars)
+ * Because they're namespaced, undefined attributes will not throw an error.
+ *
+ * @example:
+ * templateBler : BASE_MVC.wrapTemplate([
+ * '
',
- // space for title bar buttons - gen. floated to the right
- '',
- '',
-
- // expandable area for more details
- '',
- '
'
- ]);
-
- var warnings = {};
-
- var titleBarTemplate = wrapTemplate([
- // adding a tabindex here allows focusing the title bar and the use of keydown to expand the dataset display
- '
',
-//TODO: prob. belongs in dataset-list-item
- '',
- '
',
- '<%- element.name %>',
- '
',
- '',
- '
'
- ], 'element' );
-
- var subtitleTemplate = wrapTemplate([
- // override this
- ''
- ]);
-
- var detailsTemplate = wrapTemplate([
- // override this
- ''
- ]);
-
- return {
- el : elTemplato,
- warnings : warnings,
- titleBar : titleBarTemplate,
- subtitle : subtitleTemplate,
- details : detailsTemplate
+function wrapTemplate( template, jsonNamespace ){
+ jsonNamespace = jsonNamespace || 'model';
+ var templateFn = _.template( template.join( '' ) );
+ return function( json, view ){
+ var templateVars = { view : view || {}, _l : _l };
+ templateVars[ jsonNamespace ] = json || {};
+ return templateFn( templateVars );
};
-}());
+}
//==============================================================================
return {
LoggableMixin : LoggableMixin,
SessionStorageModel : SessionStorageModel,
+ mixin : mixin,
SearchableModelMixin : SearchableModelMixin,
HiddenUntilActivatedViewMixin : HiddenUntilActivatedViewMixin,
- mixin : mixin,
- wrapTemplate : wrapTemplate,
- ExpandableView : ExpandableView,
DraggableViewMixin : DraggableViewMixin,
SelectableViewMixin : SelectableViewMixin,
- ListItemView : ListItemView
+ wrapTemplate : wrapTemplate
};
});
diff --git a/static/scripts/mvc/collection/collection-li.js b/static/scripts/mvc/collection/collection-li.js
index 8c41bb33761..e2792836881 100644
--- a/static/scripts/mvc/collection/collection-li.js
+++ b/static/scripts/mvc/collection/collection-li.js
@@ -1,11 +1,12 @@
define([
+ "mvc/list/list-item",
"mvc/dataset/dataset-li",
"mvc/base-mvc",
"utils/localization"
-], function( DATASET_LI, BASE_MVC, _l ){
+], function( LIST_ITEM, DATASET_LI, BASE_MVC, _l ){
/* global Backbone, LoggableMixin */
//==============================================================================
-var ListItemView = BASE_MVC.ListItemView;
+var ListItemView = LIST_ITEM.ListItemView;
/** @class Read only view for DatasetCollection.
*/
var DCListItemView = ListItemView.extend(
@@ -97,8 +98,7 @@ var DCEListItemView = ListItemView.extend(
//TODO: this might be expendable - compacted with HDAListItemView
/** logger used to record this.log messages, commonly set to console */
- // comment this out to suppress log output
- logger : console,
+ //logger : console,
/** add the DCE class to the list item */
className : ListItemView.prototype.className + " dataset-collection-element",
diff --git a/static/scripts/mvc/collection/collection-model.js b/static/scripts/mvc/collection/collection-model.js
index e0582c07190..df42d87ddfb 100644
--- a/static/scripts/mvc/collection/collection-model.js
+++ b/static/scripts/mvc/collection/collection-model.js
@@ -270,6 +270,11 @@ var DatasetCollection = Backbone.Model
return this.save( { deleted: false }, options );
},
+ /** Is this collection deleted or purged? */
+ isDeletedOrPurged : function(){
+ return ( this.get( 'deleted' ) || this.get( 'purged' ) );
+ },
+
// ........................................................................ searchable
/** searchable attributes for collections */
searchAttributes : [
diff --git a/static/scripts/mvc/collection/collection-panel.js b/static/scripts/mvc/collection/collection-panel.js
index c655c01d1f8..1ac10b14085 100644
--- a/static/scripts/mvc/collection/collection-panel.js
+++ b/static/scripts/mvc/collection/collection-panel.js
@@ -1,9 +1,10 @@
define([
+ "mvc/list/list-panel",
"mvc/collection/collection-model",
"mvc/collection/collection-li",
"mvc/base-mvc",
"utils/localization"
-], function( DC_MODEL, DC_LI, BASE_MVC, _l ){
+], function( LIST_PANEL, DC_MODEL, DC_LI, BASE_MVC, _l ){
/* =============================================================================
TODO:
@@ -11,36 +12,29 @@ TODO:
// =============================================================================
/** @class non-editable, read-only View/Controller for a dataset collection.
*/
-var CollectionPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
+var _super = LIST_PANEL.ModelListPanel;
+var CollectionPanel = _super.extend(
/** @lends CollectionPanel.prototype */{
//MODEL is either a DatasetCollection (or subclass) or a DatasetCollectionElement (list of pairs)
/** logger used to record this.log messages, commonly set to console */
//logger : console,
- tagName : 'div',
- className : 'dataset-collection-panel',
-
- /** (in ms) that jquery effects will use */
- fxSpeed : 'fast',
+ className : _super.prototype.className + ' dataset-collection-panel',
/** sub view class used for datasets */
DatasetDCEViewClass : DC_LI.DatasetDCEListItemView,
/** sub view class used for nested collections */
NestedDCDCEViewClass : DC_LI.NestedDCDCEListItemView,
+ /** key of attribute in model to assign to this.collection */
+ modelCollectionKey : 'elements',
// ......................................................................... SET UP
/** Set up the view, set up storage, bind listeners to HistoryContents events
* @param {Object} attributes optional settings for the panel
*/
initialize : function( attributes ){
- attributes = attributes || {};
- // set the logger if requested
- if( attributes.logger ){
- this.logger = attributes.logger;
- }
- this.log( this + '.initialize:', attributes );
-
+ _super.prototype.initialize.call( this, attributes );
this.linkTarget = attributes.linkTarget || '_blank';
this.hasUser = attributes.hasUser;
@@ -50,185 +44,49 @@ var CollectionPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
//window.collectionPanel = this;
},
- /** create any event listeners for the panel
- * @fires: rendered:initial on the first render
- * @fires: empty-history when switching to a history with no HDAs or creating a new history
- */
- _setUpListeners : function(){
- // debugging
- //if( this.logger ){
- this.on( 'all', function( event ){
- this.log( this + '', arguments );
- }, this );
- //}
- return this;
- },
-
- // ------------------------------------------------------------------------ history/hda event listening
- /** listening for history and HDA events */
- _setUpModelEventHandlers : function(){
- return this;
- },
-
- // ------------------------------------------------------------------------ panel rendering
- /** Render panel
- * @fires: rendered when the panel is attached and fully visible
- * @see Backbone.View#render
- */
- render : function( speed, callback ){
- this.log( 'render:', speed, callback );
- // send a speed of 0 to have no fade in/out performed
- speed = ( speed === undefined )?( this.fxSpeed ):( speed );
- //this.debug( this + '.render, fxSpeed:', speed );
- var panel = this,
- $newRender;
-
- // handle the possibility of no model (can occur if fetching the model returns an error)
- if( !this.model ){
- return this;
- }
- $newRender = this.renderModel();
-
- // fade out existing, swap with the new, fade in, set up behaviours
- $( panel ).queue( 'fx', [
- function( next ){
- if( speed && panel.$el.is( ':visible' ) ){
- panel.$el.fadeOut( speed, next );
- } else {
- next();
- }
- },
- function( next ){
- // swap over from temp div newRender
- panel.$el.empty();
- if( $newRender ){
- panel.$el.append( $newRender.children() );
- }
- next();
- },
- function( next ){
- if( speed && !panel.$el.is( ':visible' ) ){
- panel.$el.fadeIn( speed, next );
- } else {
- next();
- }
- },
- function( next ){
- //TODO: ideally, these would be set up before the fade in (can't because of async save text)
- if( callback ){ callback.call( this ); }
- panel.trigger( 'rendered', this );
- next();
- }
- ]);
- return this;
- },
-
- /** render with collection data
- * @returns {jQuery} dom fragment as temporary container to be swapped out later
- */
- renderModel : function( ){
- // tmp div for final swap in render
-//TODO: ugh - reuse issue - refactor out
- var type = this.model.get( 'collection_type' ) || this.model.object.get( 'collection_type' ),
- json = _.extend( this.model.toJSON(), {
- parentName : this.parentName,
- type : type
- }),
- $newRender = $( '' ).append( this.templates.panel( json ) );
- this._setUpBehaviours( $newRender );
- this.renderContents( $newRender );
- return $newRender;
- },
-
- /** Set up js/widget behaviours */
- _setUpBehaviours : function( $where ){
- //TODO: these should be either sub-MVs, or handled by events
- $where = $where || this.$el;
- $where.find( '[title]' ).tooltip({ placement: 'bottom' });
- return this;
- },
-
- // ------------------------------------------------------------------------ sub-$element shortcuts
- /** the scroll container for this panel - can be $el, $el.parent(), or grandparent depending on context */
- $container : function(){
- return ( this.findContainerFn )?( this.findContainerFn.call( this ) ):( this.$el.parent() );
- },
- /** where list content views are attached */
- $datasetsList : function( $where ){
- return ( $where || this.$el ).find( '.datasets-list' );
- },
-
// ------------------------------------------------------------------------ sub-views
- /** Set up/render a view for each DCE to be shown, init with model and listeners.
- * DCE views are cached to the map this.contentViews (using the model.id as key).
- * @param {jQuery} $whereTo what dom element to prepend the DCE views to
- * @returns the number of visible DCE views
- */
- renderContents : function( $whereTo ){
- //this.debug( 'renderContents, elements:', this.model.elements );
- $whereTo = $whereTo || this.$el;
-
- this.warn( this + '.renderContents:, model:', this.model );
- var panel = this,
- contentViews = {},
- //NOTE: no filtering here
- visibleContents = this.model.getVisibleContents();
- //this.debug( 'renderContents, visibleContents:', visibleContents, $whereTo );
-
- this.$datasetsList( $whereTo ).empty();
- if( visibleContents && visibleContents.length ){
- visibleContents.each( function( content ){
- var contentId = content.id,
- contentView = panel._createContentView( content );
- contentViews[ contentId ] = contentView;
- panel._attachContentView( contentView.render(), $whereTo );
- });
- }
- this.contentViews = contentViews;
- return this.contentViews;
+ /** In this override, use model.getVisibleContents */
+ _filterCollection : function(){
+//TODO: should *not* be model.getVisibleContents
+ return this.model.getVisibleContents();
},
- /** */
- _createContentView : function( content ){
- //this.debug( 'content json:', JSON.stringify( content, null, ' ' ) );
- var contentView = null,
- ContentClass = this._getContentClass( content );
- //this.debug( 'content:', content );
- //this.debug( 'ContentClass:', ContentClass );
- contentView = new ContentClass({
- model : content,
- linkTarget : this.linkTarget,
- //draggable : true,
- hasUser : this.hasUser,
- logger : this.logger
- });
- //this.debug( 'contentView:', contentView );
- this._setUpContentListeners( contentView );
- return contentView;
- },
-
- /** */
- _getContentClass : function( content ){
- //this.debug( this + '._getContentClass:', content );
+ /** override to return proper view class based on element_type */
+ _getItemViewClass : function( model ){
+ //this.debug( this + '._getItemViewClass:', model );
//TODO: subclasses use DCEViewClass - but are currently unused - decide
- switch( content.get( 'element_type' ) ){
+ switch( model.get( 'element_type' ) ){
case 'hda':
return this.DatasetDCEViewClass;
case 'dataset_collection':
return this.NestedDCDCEViewClass;
}
- throw new TypeError( 'Unknown element type:', content.get( 'element_type' ) );
+ throw new TypeError( 'Unknown element type:', model.get( 'element_type' ) );
},
- /** Set up listeners for content view events. In this override, handle collection expansion. */
- _setUpContentListeners : function( contentView ){
+ /** override to add link target and anon */
+ _getItemViewOptions : function( model ){
+ var options = _super.prototype._getItemViewOptions.call( this, model );
+ return _.extend( options, {
+ linkTarget : this.linkTarget,
+ hasUser : this.hasUser
+ });
+ },
+
+ /** when a sub-view is clicked in the collection panel that is itself a collection,
+ * hide this panel's elements and show the sub-collection in its own panel.
+ */
+ _setUpItemViewListeners : function( view ){
var panel = this;
- if( contentView.model.get( 'element_type' ) === 'dataset_collection' ){
- contentView.on( 'expanded', function( collectionView ){
+ _super.prototype._setUpItemViewListeners.call( panel, view );
+ //TODO:?? doesn't seem to belong here
+ if( view.model.get( 'element_type' ) === 'dataset_collection' ){
+ view.on( 'expanded', function( collectionView ){
panel.info( 'expanded', collectionView );
panel._addCollectionPanel( collectionView );
});
}
+ return panel;
},
/** When a sub-collection is clicked, hide the current panel and render the sub-collection in its own panel */
@@ -246,7 +104,7 @@ var CollectionPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
});
currPanel.panelStack.push( panel );
- currPanel.$( '.controls' ).add( '.datasets-list' ).hide();
+ currPanel.$( '.controls' ).add( '.list-items' ).hide();
currPanel.$el.append( panel.$el );
panel.on( 'close', function(){
currPanel.render();
@@ -266,14 +124,6 @@ var CollectionPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
}
},
- /** attach an contentView to the panel */
- _attachContentView : function( contentView, $whereTo ){
- $whereTo = $whereTo || this.$el;
- var $datasetsList = this.$datasetsList( $whereTo );
- $datasetsList.append( contentView.$el );
- return this;
- },
-
// ------------------------------------------------------------------------ panel events
/** event map */
events : {
@@ -293,16 +143,16 @@ var CollectionPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
}
});
-//----------------------------------------------------------------------------- TEMPLATES
-/** underscore templates */
-CollectionPanel.templates = CollectionPanel.prototype.templates = (function(){
-// use closure to run underscore template fn only once at module load
- var _panelTemplate = _.template([
+
+//------------------------------------------------------------------------------ TEMPLATES
+CollectionPanel.prototype.templates = (function(){
+
+ var controlsTemplate = BASE_MVC.wrapTemplate([
'
',
//TODO: remove logic from template
- '<% if( collection.type === "list" ){ %>',
+ '<% if( collection.collection_type === "list" ){ %>',
_l( 'a list of datasets' ),
- '<% } else if( collection.type === "paired" ){ %>',
+ '<% } else if( collection.collection_type === "paired" ){ %>',
_l( 'a pair of datasets' ),
- '<% } else if( collection.type === "list:paired" ){ %>',
+ '<% } else if( collection.collection_type === "list:paired" ){ %>',
_l( 'a list of paired datasets' ),
'<% } %>',
'
',
'
',
- '',
- // where the datasets/hdas are added
- ''
- ].join( '' ));
+ ''
+ ], 'collection' );
- // we override here in order to pass the localizer (_L) into the template scope - since we use it as a fn within
- return {
- panel : function( json ){
- return _panelTemplate({ _l: _l, collection: json });
- }
- };
+ return _.extend( _.clone( _super.prototype.templates ), {
+ controls : controlsTemplate
+ });
}());
+
// =============================================================================
/** @class non-editable, read-only View/Controller for a dataset collection. */
var ListCollectionPanel = CollectionPanel.extend(
diff --git a/static/scripts/mvc/dataset/dataset-li.js b/static/scripts/mvc/dataset/dataset-li.js
index 16acfc4eb9f..583e0121b40 100644
--- a/static/scripts/mvc/dataset/dataset-li.js
+++ b/static/scripts/mvc/dataset/dataset-li.js
@@ -1,8 +1,9 @@
define([
+ "mvc/list/list-item",
"mvc/dataset/states",
"mvc/base-mvc",
"utils/localization"
-], function( STATES, BASE_MVC, _l ){
+], function( LIST_ITEM, STATES, BASE_MVC, _l ){
/* global Backbone */
/*==============================================================================
TODO:
@@ -11,7 +12,7 @@ TODO:
simplify button rendering
==============================================================================*/
-var _super = BASE_MVC.ListItemView;
+var _super = LIST_ITEM.ListItemView;
/** @class Read only list view for either LDDAs, HDAs, or HDADCEs.
* Roughly, any DatasetInstance (and not a raw Dataset).
*/
diff --git a/static/scripts/mvc/dataset/dataset-list.js b/static/scripts/mvc/dataset/dataset-list.js
index 8d8d4108eec..fb606663171 100644
--- a/static/scripts/mvc/dataset/dataset-list.js
+++ b/static/scripts/mvc/dataset/dataset-list.js
@@ -1,5 +1,5 @@
define([
- "mvc/dataset/list-panel",
+ "mvc/list/list-panel",
"mvc/dataset/dataset-li",
"mvc/base-mvc",
"utils/localization"
diff --git a/static/scripts/mvc/dataset/dataset-model.js b/static/scripts/mvc/dataset/dataset-model.js
index afd82bea38b..a2a365e118c 100644
--- a/static/scripts/mvc/dataset/dataset-model.js
+++ b/static/scripts/mvc/dataset/dataset-model.js
@@ -94,6 +94,9 @@ var DatasetAssociation = Backbone.Model.extend( BASE_MVC.LoggableMixin ).extend(
this.trigger( 'state:ready', currModel, newState, this.previous( 'state' ) );
}
});
+ this.on( 'change:urls', function(){
+ console.warn( 'change:urls', arguments );
+ });
// the download url (currenlty) relies on having a correct file extension
this.on( 'change:id change:file_ext', function( currModel ){
this._generateUrls();
@@ -104,6 +107,8 @@ var DatasetAssociation = Backbone.Model.extend( BASE_MVC.LoggableMixin ).extend(
/** override to add urls */
toJSON : function(){
var json = Backbone.Model.prototype.toJSON.call( this );
+ //console.warn( 'returning json?' );
+ //return json;
return _.extend( json, {
urls : this.urls
});
diff --git a/static/scripts/mvc/history/hda-li.js b/static/scripts/mvc/history/hda-li.js
index 8eae89c9d75..a54c679af76 100644
--- a/static/scripts/mvc/history/hda-li.js
+++ b/static/scripts/mvc/history/hda-li.js
@@ -17,12 +17,12 @@ var HDAListItemView = _super.extend(
/** logger used to record this.log messages, commonly set to console */
//logger : console,
+ className : _super.prototype.className + " history-content",
+
initialize : function( attributes, options ){
_super.prototype.initialize.call( this, attributes, options );
},
- className : _super.prototype.className + " history-content",
-
// ......................................................................... misc
/** String representation */
toString : function(){
diff --git a/static/scripts/mvc/history/history-contents.js b/static/scripts/mvc/history/history-contents.js
index 51b45469531..4a0f8b6d8b3 100644
--- a/static/scripts/mvc/history/history-contents.js
+++ b/static/scripts/mvc/history/history-contents.js
@@ -53,6 +53,7 @@ var HistoryContents = Backbone.Collection.extend( BASE_MVC.LoggableMixin ).exten
*/
initialize : function( models, options ){
options = options || {};
+//TODO: could probably use the contents.history_id instead
this.historyId = options.historyId;
//this._setUpListeners();
@@ -226,7 +227,7 @@ var HistoryContents = Backbone.Collection.extend( BASE_MVC.LoggableMixin ).exten
if( !existing ){ return model; }
// merge the models _BEFORE_ calling the superclass version
- var merged = existing.toJSON();
+ var merged = _.clone( existing.attributes );
_.extend( merged, model );
return merged;
});
@@ -303,6 +304,13 @@ var HistoryContents = Backbone.Collection.extend( BASE_MVC.LoggableMixin ).exten
return xhr;
},
+ /** In this override, copy the historyId to the clone */
+ clone : function(){
+ var clone = Backbone.Collection.prototype.clone.call( this );
+ clone.historyId = this.historyId;
+ return clone;
+ },
+
/** debugging */
print : function(){
var contents = this;
diff --git a/static/scripts/mvc/history/history-model.js b/static/scripts/mvc/history/history-model.js
index c6ef019f0e1..d33ce1be61f 100644
--- a/static/scripts/mvc/history/history-model.js
+++ b/static/scripts/mvc/history/history-model.js
@@ -232,7 +232,7 @@ History.UPDATE_DELAY = 4000;
/** Get data for a history then its hdas using a sequential ajax call, return a deferred to receive both */
History.getHistoryData = function getHistoryData( historyId, options ){
options = options || {};
- var hdaDetailIds = options.hdaDetailIds || [];
+ var detailIdsFn = options.detailIdsFn || [];
var hdcaDetailIds = options.hdcaDetailIds || [];
//console.debug( 'getHistoryData:', historyId, options );
@@ -250,20 +250,20 @@ History.getHistoryData = function getHistoryData( historyId, options ){
// get the number of hdas accrd. to the history
return historyData && historyData.empty;
}
- function getHdas( historyData ){
+ function getContents( historyData ){
// get the hda data
// if no hdas accrd. to history: return empty immed.
if( isEmpty( historyData ) ){ return []; }
// if there are hdas accrd. to history: get those as well
- if( _.isFunction( hdaDetailIds ) ){
- hdaDetailIds = hdaDetailIds( historyData );
+ if( _.isFunction( detailIdsFn ) ){
+ detailIdsFn = detailIdsFn( historyData );
}
if( _.isFunction( hdcaDetailIds ) ){
hdcaDetailIds = hdcaDetailIds( historyData );
}
var data = {};
- if( hdaDetailIds.length ) {
- data.dataset_details = hdaDetailIds.join( ',' );
+ if( detailIdsFn.length ) {
+ data.dataset_details = detailIdsFn.join( ',' );
}
if( hdcaDetailIds.length ) {
// for symmetry, not actually used by backend of consumed
@@ -274,10 +274,10 @@ History.getHistoryData = function getHistoryData( historyId, options ){
}
// getting these concurrently is 400% slower (sqlite, local, vanilla) - so:
- // chain the api calls - getting history first then hdas
+ // chain the api calls - getting history first then contents
var historyFn = options.historyFn || getHistory,
- hdaFn = options.hdaFn || getHdas;
+ contentsFn = options.contentsFn || getContents;
// chain ajax calls: get history first, then hdas
var historyXHR = historyFn( historyId );
@@ -291,15 +291,15 @@ History.getHistoryData = function getHistoryData( historyId, options ){
df.reject( xhr, 'loading the history' );
});
- var hdaXHR = historyXHR.then( hdaFn );
- hdaXHR.then( function( hdaJSON ){
- df.notify({ status: 'dataset data retrieved', historyJSON: historyJSON, hdaJSON: hdaJSON });
+ var contentsXHR = historyXHR.then( contentsFn );
+ contentsXHR.then( function( contentsJSON ){
+ df.notify({ status: 'contents data retrieved', historyJSON: historyJSON, contentsJSON: contentsJSON });
// we've got both: resolve the outer scope deferred
- df.resolve( historyJSON, hdaJSON );
+ df.resolve( historyJSON, contentsJSON );
});
- hdaXHR.fail( function( xhr, status, message ){
+ contentsXHR.fail( function( xhr, status, message ){
// call reject on the outer deferred to allow its fail callback to run
- df.reject( xhr, 'loading the datasets', { history: historyJSON } );
+ df.reject( xhr, 'loading the contents', { history: historyJSON } );
});
return df;
diff --git a/static/scripts/mvc/history/history-panel-annotated.js b/static/scripts/mvc/history/history-panel-annotated.js
index 4144b0c7fb2..015ecd4e231 100644
--- a/static/scripts/mvc/history/history-panel-annotated.js
+++ b/static/scripts/mvc/history/history-panel-annotated.js
@@ -1,118 +1,97 @@
define([
"mvc/history/history-panel",
"mvc/history/hda-li",
+ "mvc/history/hdca-li",
+ "mvc/base-mvc",
"utils/localization"
-], function( HPANEL, HDA_LI, _l ){
+], function( HPANEL, HDA_LI, HDCA_LI, BASE_MVC, _l ){
/* =============================================================================
TODO:
============================================================================= */
-var _super = HPANEL.ReadOnlyHistoryPanel;
+var _super = HPANEL.HistoryPanel;
// used in history/display.mako and history/embed.mako
/** @class View/Controller for a tabular view of the history model.
- * @name AnnotatedHistoryPanel
*
* As ReadOnlyHistoryPanel, but with:
* history annotation always shown
* datasets displayed in a table:
* datasets in left cells, dataset annotations in the right
- *
- * @augments Backbone.View
- * @borrows LoggableMixin#logger as #logger
- * @borrows LoggableMixin#log as #log
- * @constructs
*/
var AnnotatedHistoryPanel = _super.extend(
/** @lends AnnotatedHistoryPanel.prototype */{
/** logger used to record this.log messages, commonly set to console */
- // comment this out to suppress log output
//logger : console,
- className : 'annotated-history-panel',
-
- //TODO:?? possibly into own annotated class
- /** class to use for constructing the HDA views */
- //HDAViewClass : HDA_LI.HDAListItemView,
+ className : _super.prototype.className + ' annotated-history-panel',
// ------------------------------------------------------------------------ panel rendering
- /** render with history data
- * In this override:
- * replace the datasets list with a table,
- * add the history annotation,
- * and move the search controls
- * @returns {jQuery} dom fragment as temporary container to be swapped out later
+ /** In this override, add the history annotation
*/
- renderModel : function( ){
- // why do we need this here? why isn't className being applied?
- this.$el.addClass( this.className );
- var $newRender = _super.prototype.renderModel.call( this ),
- // move datasets from div to table
- $datasetsList = this.$datasetsList( $newRender ),
- $datasetsTable = $( '
' ).addClass( 'datasets-list datasets-table' );
- $datasetsTable.append( $datasetsList.children() );
- $datasetsList.replaceWith( $datasetsTable );
- //TODO: it's possible to do this with css only, right? display: table-cell, etc.?
-
- // add history annotation under subtitle
- $newRender.find( '.history-subtitle' ).after( this.renderHistoryAnnotation() );
-
- // hide search button, move search bar beneath controls (instead of above title), show, and set up
- $newRender.find( '.history-search-btn' ).hide();
- $newRender.find( '.history-controls' ).after( $newRender.find( '.history-search-controls' ).show() );
-
+ _buildNewRender : function(){
+ //TODO: shouldn't this display regardless (on all non-current panels)?
+ var $newRender = _super.prototype._buildNewRender.call( this );
+ this.renderHistoryAnnotation( $newRender );
return $newRender;
},
/** render the history's annotation as its own field */
- renderHistoryAnnotation : function(){
+ renderHistoryAnnotation : function( $newRender ){
var annotation = this.model.get( 'annotation' );
- if( !annotation ){ return null; }
- return $([
- '
', annotation, '
'
- ].join( '' ));
+ if( !annotation ){ return; }
+ $newRender.find( '.controls .annotation-display' ).text( annotation );
},
- /** Set up/render a view for each HDA to be shown, init with model and listeners.
- * In this override, add table header cells to indicate the dataset, annotation columns
+ /** In this override, convert the list-items tag to a table
+ * and add table header cells to indicate the dataset, annotation columns
*/
- renderHdas : function( $whereTo ){
+ renderItems : function( $whereTo ){
$whereTo = $whereTo || this.$el;
- var hdaViews = _super.prototype.renderHdas.call( this, $whereTo );
- this.$datasetsList( $whereTo ).prepend( $( '
' ).text( _l( 'Annotation' ) )
+ ]));
+ return views;
},
- // ------------------------------------------------------------------------ hda sub-views
- /** attach an hdaView to the panel
- * In this override, wrap the hdaView in a table row and cell, adding a 2nd cell for the hda annotation
+ // ------------------------------------------------------------------------ sub-views
+ /** In this override, wrap the content view in a table row
+ * with the content in the left td and annotation/extra-info in the right
*/
- attachContentView : function( hdaView, $whereTo ){
- $whereTo = $whereTo || this.$el;
- // build a row around the dataset with the std hdaView in the first cell and the annotation in the next
- var stateClass = _.find( hdaView.el.classList, function( c ){ return ( /^state\-/ ).test( c ); }),
- annotation = hdaView.model.get( 'annotation' ) || '',
- $tr = $( '
' ).addClass( 'dataset-row' ).append([
- $( '
' ).addClass( 'dataset-container' ).append( hdaView.$el )
- // visually match the cell bg to the dataset at runtime (prevents the empty space)
- // (getting bg via jq on hidden elem doesn't work on chrome/webkit - so use states)
- //.css( 'background-color', hdaView.$el.css( 'background-color' ) ),
- .addClass( stateClass? stateClass.replace( '-', '-color-' ): '' ),
- $( '
' ).addClass( 'additional-info' ).text( annotation )
- ]);
- this.$datasetsList( $whereTo ).append( $tr );
+ _attachItems : function( $whereTo ){
+ this.$list( $whereTo ).append( this.views.map( function( view ){
+ //TODO:?? possibly make this more flexible: instead of annotation use this._additionalInfo()
+ // build a row around the dataset with the std itemView in the first cell and the annotation in the next
+ var stateClass = _.find( view.el.classList, function( c ){ return ( /^state\-/ ).test( c ); }),
+ annotation = view.model.get( 'annotation' ) || '',
+ $tr = $( '
' ).append([
+ $( '
' ).addClass( 'contents-container' ).append( view.$el )
+ // visually match the cell bg to the dataset at runtime (prevents the empty space)
+ // (getting bg via jq on hidden elem doesn't work on chrome/webkit - so use states)
+ //.css( 'background-color', view.$el.css( 'background-color' ) ),
+ .addClass( stateClass? stateClass.replace( '-', '-color-' ): '' ),
+ $( '
' ).addClass( 'additional-info' ).text( annotation )
+ ]);
+ return $tr;
+ }));
+ return this;
},
// ------------------------------------------------------------------------ panel events
/** event map */
events : _.extend( _.clone( _super.prototype.events ), {
+ // clicking on any part of the row will expand the items
'click tr' : function( ev ){
- //if( !ev.target.hasAttribute( 'href' ) ){
- $( ev.currentTarget ).find( '.dataset-title-bar' ).click();
- //}
+ $( ev.currentTarget ).find( '.title-bar' ).click();
},
// prevent propagation on icon btns so they won't bubble up to tr and toggleBodyVisibility
'click .icon-btn' : function( ev ){
@@ -127,6 +106,7 @@ var AnnotatedHistoryPanel = _super.extend(
}
});
+
//==============================================================================
return {
AnnotatedHistoryPanel : AnnotatedHistoryPanel
diff --git a/static/scripts/mvc/history/history-panel-edit-current.js b/static/scripts/mvc/history/history-panel-edit-current.js
index 74fab60b35f..447f75e0a35 100644
--- a/static/scripts/mvc/history/history-panel-edit-current.js
+++ b/static/scripts/mvc/history/history-panel-edit-current.js
@@ -11,8 +11,6 @@ define([
var HistoryPanelPrefs = BASE_MVC.SessionStorageModel.extend(
/** @lends HistoryPanelPrefs.prototype */{
defaults : {
- /** is the panel currently showing the search/filter controls? */
- searching : false,
/** should the tags editor be shown or hidden initially? */
tagsEditorShown : false,
/** should the annotation editor be shown or hidden initially? */
@@ -32,31 +30,26 @@ HistoryPanelPrefs.storageKey = function storageKey(){
TODO:
============================================================================= */
-var _super = HPANEL_EDIT.HistoryPanel;
+var _super = HPANEL_EDIT.HistoryPanelEdit;
// used in root/index.mako
/** @class View/Controller for the user's current history model as used in the history
* panel (current right hand panel) of the analysis page.
- * @name HistoryPanel
*
- * @augments Backbone.View
- * @borrows LoggableMixin#logger as #logger
- * @borrows LoggableMixin#log as #log
- * @constructs
+ * The only history panel that will poll for updates.
*/
var CurrentHistoryPanel = _super.extend(
/** @lends CurrentHistoryPanel.prototype */{
+ className : _super.prototype.className + ' current-history-panel',
+
/** logger used to record this.log messages, commonly set to console */
- // comment this out to suppress log output
//logger : console,
emptyMsg : _l( "This history is empty. Click 'Get Data' on the left tool menu to start" ),
noneFoundMsg : _l( "No matching datasets found" ),
// ......................................................................... SET UP
- /** Set up the view, set up storage, bind listeners to HistoryContents events
- * @param {Object} attributes
- */
+ /** Set up the view, set up storage, bind listeners to HistoryContents events */
initialize : function( attributes ){
attributes = attributes || {};
@@ -72,19 +65,19 @@ var CurrentHistoryPanel = _super.extend(
this.panelStack = [];
},
- // ------------------------------------------------------------------------ loading history/hda models
- /** (re-)loads the user's current history & hdas w/ details */
+ // ------------------------------------------------------------------------ loading history/item models
+ /** (re-)loads the user's current history & contents w/ details */
loadCurrentHistory : function( attributes ){
this.debug( this + '.loadCurrentHistory' );
// implemented as a 'fresh start' or for when there is no model (intial panel render)
var panel = this;
- return this.loadHistoryWithHDADetails( 'current', attributes )
- .then(function( historyData, hdaData ){
+ return this.loadHistoryWithDetails( 'current', attributes )
+ .then(function( historyData, contentsData ){
panel.trigger( 'current-history', panel );
});
},
- /** loads a history & hdas w/ details and makes them the current history */
+ /** loads a history & contents w/ details and makes them the current history */
switchToHistory : function( historyId, attributes ){
//this.info( 'switchToHistory:', historyId, attributes );
var panel = this,
@@ -94,8 +87,8 @@ var CurrentHistoryPanel = _super.extend(
// method : 'PUT'
//});
};
- return this.loadHistoryWithHDADetails( historyId, attributes, historyFn )
- .then(function( historyData, hdaData ){
+ return this.loadHistoryWithDetails( historyId, attributes, historyFn )
+ .then(function( historyData, contentsData ){
panel.trigger( 'switched-history', panel );
});
},
@@ -115,7 +108,7 @@ var CurrentHistoryPanel = _super.extend(
// id undefined bc there is no historyId yet - the server will provide
// (no need for details - nothing expanded in new history)
return this.loadHistory( undefined, attributes, historyFn )
- .then(function( historyData, hdaData ){
+ .then(function( historyData, contentsData ){
panel.trigger( 'new-history', panel );
});
},
@@ -130,98 +123,60 @@ var CurrentHistoryPanel = _super.extend(
return this;
},
- // ------------------------------------------------------------------------ history/hda event listening
- /** listening for history and HDA events */
- _setUpModelEventHandlers : function(){
- _super.prototype._setUpModelEventHandlers.call( this );
- // ---- history
- // update the quota meter when current history changes size
- if( Galaxy && Galaxy.quotaMeter ){
- this.listenTo( this.model, 'change:nice_size', function(){
- //this.info( '!! model size changed:', this.model.get( 'nice_size' ) )
-//TODO: global
- Galaxy.quotaMeter.update();
- });
- }
+ // ------------------------------------------------------------------------ history/content event listening
+ /** listening for collection events */
+ _setUpCollectionListeners : function(){
+ _super.prototype._setUpCollectionListeners.call( this );
- // if an a hidden hda is created (gen. by a workflow), moves thru the updater to the ready state,
+ // if a hidden item is created (gen. by a workflow), moves thru the updater to the ready state,
// then: remove it from the collection if the panel is set to NOT show hidden datasets
- this.model.contents.on( 'state:ready', function( hda, newState, oldState ){
- if( ( !hda.get( 'visible' ) )
+ this.collection.on( 'state:ready', function( model, newState, oldState ){
+ if( ( !model.get( 'visible' ) )
&& ( !this.storage.get( 'show_hidden' ) ) ){
- this.removeHdaView( this.hdaViews[ hda.id ] );
+ this.removeItemView( this.viewFromModel( model ) );
}
}, this );
},
- // ------------------------------------------------------------------------ panel rendering
- /** Render urls, historyPanel body, and hdas (if any are shown)
- * @fires: rendered when the panel is attached and fully visible
- * @see Backbone.View#render
- */
- render : function( speed, callback ){
- this.log( 'render:', speed, callback );
- // send a speed of 0 to have no fade in/out performed
- speed = ( speed === undefined )?( this.fxSpeed ):( speed );
- var panel = this,
- $newRender;
-
- // handle the possibility of no model (can occur if fetching the model returns an error)
- if( this.model ){
- $newRender = this.renderModel();
- } else {
- $newRender = this.renderWithoutModel();
+ /** listening for history events */
+ _setUpModelListeners : function(){
+ _super.prototype._setUpModelListeners.call( this );
+ // ---- history
+ // update the quota meter when current history changes size
+//TODO: global - have Galaxy listen to this instead
+ if( Galaxy && Galaxy.quotaMeter ){
+ this.listenTo( this.model, 'change:nice_size', function(){
+ //this.info( '!! model size changed:', this.model.get( 'nice_size' ) )
+ Galaxy.quotaMeter.update();
+ });
}
- // fade out existing, swap with the new, fade in, set up behaviours
- $( panel ).queue( 'fx', [
- function( next ){
- if( speed && panel.$el.is( ':visible' ) ){
- panel.$el.fadeOut( speed, next );
- } else {
- next();
- }
- },
- function( next ){
- // swap over from temp div newRender
- panel.$el.empty();
- if( $newRender ){
- panel.$el.append( $newRender.children() );
- panel.renderBasedOnPrefs();
- }
- next();
- },
- function( next ){
- if( speed && !panel.$el.is( ':visible' ) ){
- panel.$el.fadeIn( speed, next );
- } else {
- next();
- }
- },
- function( next ){
- //TODO: ideally, these would be set up before the fade in (can't because of async save text)
- if( callback ){ callback.call( this ); }
- panel.trigger( 'rendered', this );
- next();
- }
- ]);
- return this;
},
- /** perform additional rendering based on preferences */
- renderBasedOnPrefs : function(){
- if( this.preferences.get( 'searching' ) ){
- this.toggleSearchControls( 0, true );
- }
+ // ------------------------------------------------------------------------ panel rendering
+ /** In this override, handle null models and move the search input to the top */
+ _buildNewRender : function(){
+ if( !this.model ){ return $(); }
+ var $newRender = _super.prototype._buildNewRender.call( this );
+ //TODO: hacky
+ $newRender.find( '.search' ).prependTo( $newRender.find( '.controls' ) );
+ this._renderQuotaMessage( $newRender );
+ return $newRender;
+ },
+
+ /** render the message displayed when a user is over quota and can't run jobs */
+ _renderQuotaMessage : function( $whereTo ){
+ $whereTo = $whereTo || this.$el;
+ return $( this.templates.quotaMsg( {}, this ) ).prependTo( $whereTo.find( '.messages' ) );
},
/** In this override, add links to open data uploader or get data in the tools section */
- _renderEmptyMsg : function( $whereTo ){
+ _renderEmptyMessage : function( $whereTo ){
var panel = this,
$emptyMsg = panel.$emptyMessage( $whereTo ),
$toolMenu = $( '.toolMenuContainer' );
- if( ( _.isEmpty( panel.hdaViews ) && !panel.searchFor )
+ if( ( _.isEmpty( panel.views ) && !panel.searchFor )
&& ( Galaxy && Galaxy.upload && $toolMenu.size() ) ){
$emptyMsg.empty();
@@ -246,25 +201,12 @@ var CurrentHistoryPanel = _super.extend(
// $( this ).fadeTo( 200, 1.0 );
//});
});
-
- $emptyMsg.show();
-
-
- } else {
- _super.prototype._renderEmptyMsg.call( this, $whereTo );
+ return $emptyMsg.show();
}
- return this;
+ return _super.prototype._renderEmptyMessage.call( this, $whereTo );
},
- /** In this override, save the search control visibility state to preferences */
- toggleSearchControls : function( eventOrSpeed, show ){
- var visible = _super.prototype.toggleSearchControls.call( this, eventOrSpeed, show );
- this.preferences.set( 'searching', visible );
- },
-
- /** render the tag sub-view controller
- * In this override, get and set current panel preferences when editor is used
- */
+ /** In this override, get and set current panel preferences when editor is used */
_renderTags : function( $where ){
var panel = this;
// render tags and show/hide based on preferences
@@ -278,9 +220,7 @@ var CurrentHistoryPanel = _super.extend(
panel.preferences.set( 'tagsEditorShown', tagsEditor.hidden );
});
},
- /** render the annotation sub-view controller
- * In this override, get and set current panel preferences when editor is used
- */
+ /** In this override, get and set current panel preferences when editor is used */
_renderAnnotation : function( $where ){
var panel = this;
// render annotation and show/hide based on preferences
@@ -296,54 +236,62 @@ var CurrentHistoryPanel = _super.extend(
},
// ------------------------------------------------------------------------ sub-views
- /** Set up HistoryPanel listeners for HDAView events. In this override, handle collection expansion.
- */
- _setUpHdaListeners : function( hdaView ){
- _super.prototype._setUpHdaListeners.call( this, hdaView );
+ /** Override to reverse order of views - newest contents on top */
+ _attachItems : function( $whereTo ){
+ this.$list( $whereTo ).append( this.views.reverse().map( function( view ){
+ return view.$el;
+ }));
+ return this;
+ },
- var historyView = this;
- if( hdaView instanceof historyView.HDCAViewClass ){
- hdaView.off( 'expanded' );
- hdaView.on( 'expanded', function( collectionView ){
- this.info( 'expanded', collectionView );
- historyView._addCollectionPanel( collectionView );
+ /** In this override, handle collection expansion. */
+ _setUpItemViewListeners : function( view ){
+ var panel = this;
+ _super.prototype._setUpItemViewListeners.call( panel, view );
+
+ if( view instanceof panel.HDCAViewClass ){
+ view.off( 'expanded' );
+ view.on( 'expanded', function( collectionView ){
+ panel.info( 'expanded', collectionView );
+ panel._addCollectionPanel( collectionView );
});
- hdaView.off( 'collapsed' );
+ view.off( 'collapsed' );
}
},
+ /** In this override, handle collection expansion. */
_addCollectionPanel : function( collectionView ){
- var historyView = this,
+ var panel = this,
collectionModel = collectionView.model;
this.debug( 'history panel (stack), collectionModel:', collectionModel );
- var panel = new ( this._getCollectionPanelClass( collectionModel ) )({
+ var overlaid = new ( this._getCollectionPanelClass( collectionModel ) )({
model : collectionModel,
HDAViewClass : this.HDAViewClass,
parentName : this.model.get( 'name' ),
linkTarget : this.linkTarget
});
- historyView.panelStack.push( panel );
+ panel.panelStack.push( overlaid );
- historyView.$( '.history-controls' ).add( '.datasets-list' ).hide();
- historyView.$el.append( panel.$el );
- panel.on( 'close', function(){
- historyView.render();
+ panel.$( '.controls' ).add( panel.$list() ).hide();
+ panel.$el.append( overlaid.$el );
+ overlaid.on( 'close', function(){
+ panel.render();
collectionView.collapse();
- historyView.panelStack.pop();
+ panel.panelStack.pop();
});
//TODO: to hdca-model, hasDetails
- if( !panel.model.hasDetails() ){
- var xhr = panel.model.fetch();
+ if( !overlaid.model.hasDetails() ){
+ var xhr = overlaid.model.fetch();
xhr.done( function(){
//this.debug( 'collection data fetched' );
- //this.debug( JSON.stringify( panel.model.toJSON(), null, ' ' ) );
+ //this.debug( JSON.stringify( overlaid.model.toJSON(), null, ' ' ) );
//TODO: (re-)render collection contents
- panel.render();
+ overlaid.render();
});
} else {
- panel.render();
+ overlaid.render();
}
},
@@ -359,18 +307,43 @@ var CurrentHistoryPanel = _super.extend(
throw new TypeError( 'Uknown collection_type: ' + model.get( 'collection_type' ) );
},
- /** In this override, check for any overlaid panels first
- * @param {HistoryDatasetAssociation} hda
- */
- addContentView : function( hda ){
+ /** In this override, check for any overlaid panels first */
+ addItemView : function( model, collection, options ){
+ this.log( this + '.addItemView:', model );
+ if( !this._filterItem( model ) ){ return undefined; }
+
+ var panel = this;
+ // don't show/add if there are panels overlaid on this one
if( this.panelStack.length ){
- return this;
+ return panel;
}
- return _super.prototype.addContentView.call( this, hda );
+
+ // current history item order is reversed - use unshift and prepend
+ var view = panel._createItemView( model );
+ this.views.unshift( view );
+ panel.scrollToTop();
+
+ $({}).queue([
+ function fadeOutEmptyMsg( next ){
+ var $emptyMsg = panel.$emptyMessage();
+ if( $emptyMsg.is( ':visible' ) ){
+ $emptyMsg.fadeOut( panel.fxSpeed, next );
+ } else {
+ next();
+ }
+ },
+ function createAndPrepend( next ){
+ //view.render().$el.hide();
+ panel.scrollToTop();
+ panel.$list().prepend( view.render( 0 ).$el.hide() );
+ view.$el.slideDown( panel.fxSpeed );
+ }
+ ]);
+ return view;
},
// ........................................................................ external objects/MVC
- //TODO: remove quota meter from panel and remove this
+//TODO: remove quota meter from panel and remove this
/** add listeners to an external quota meter (mvc/user/user-quotameter.js) */
connectToQuotaMeter : function( quotaMeter ){
if( !quotaMeter ){
@@ -431,6 +404,22 @@ var CurrentHistoryPanel = _super.extend(
});
+//------------------------------------------------------------------------------ TEMPLATES
+CurrentHistoryPanel.prototype.templates = (function(){
+
+ var quotaMsgTemplate = BASE_MVC.wrapTemplate([
+ '
',
+ _l( 'You are over your disk quota' ), '. ',
+ _l( 'Tool execution is on hold until your disk usage drops below your allocated quota' ), '.',
+ '
'
+ ], 'history' );
+ return _.extend( _.clone( _super.prototype.templates ), {
+ quotaMsg : quotaMsgTemplate
+ });
+
+}());
+
+
//==============================================================================
return {
CurrentHistoryPanel : CurrentHistoryPanel
diff --git a/static/scripts/mvc/history/history-panel-edit.js b/static/scripts/mvc/history/history-panel-edit.js
index 304c1a56dc2..ad561d548c9 100644
--- a/static/scripts/mvc/history/history-panel-edit.js
+++ b/static/scripts/mvc/history/history-panel-edit.js
@@ -23,24 +23,18 @@ define([
TODO:
============================================================================= */
-var _super = HPANEL.ReadOnlyHistoryPanel;
+var _super = HPANEL.HistoryPanel;
// base class for current-history-panel and used as-is in history/view.mako
/** @class Editable View/Controller for the history model.
- * @name HistoryPanel
*
* Allows:
- * (everything ReadOnlyHistoryPanel allows)
+ * (everything HistoryPanel allows)
* changing the name
* displaying and editing tags and annotations
- * multi-selection and operations on mulitple hdas
- *
- * @augments Backbone.View
- * @borrows LoggableMixin#logger as #logger
- * @borrows LoggableMixin#log as #log
- * @constructs
+ * multi-selection and operations on mulitple content items
*/
-var HistoryPanel = _super.extend(
-/** @lends HistoryPanel.prototype */{
+var HistoryPanelEdit = _super.extend(
+/** @lends HistoryPanelEdit.prototype */{
/** logger used to record this.log messages, commonly set to console */
//logger : console,
@@ -56,13 +50,9 @@ var HistoryPanel = _super.extend(
*/
initialize : function( attributes ){
attributes = attributes || {};
+ _super.prototype.initialize.call( this, attributes );
// ---- set up instance vars
- /** selected hda ids */
- this.selectedHdaIds = [];
- /** last selected hda */
- this.lastSelectedViewId = null;
-
/** editor for tags - sub-view */
this.tagsEditor = null;
/** editor for annotations - sub-view */
@@ -73,52 +63,44 @@ var HistoryPanel = _super.extend(
// states/modes the panel can be in
/** is the panel currently showing the dataset selection controls? */
- this.selecting = attributes.selecting || false;
this.annotationEditorShown = attributes.annotationEditorShown || false;
this.tagsEditorShown = attributes.tagsEditorShown || false;
- _super.prototype.initialize.call( this, attributes );
+ this.multiselectActions = attributes.multiselectActions || this._getActions();
},
// ------------------------------------------------------------------------ panel rendering
- /** listening for history and HDA events */
- _setUpModelEventHandlers : function(){
- _super.prototype._setUpModelEventHandlers.call( this );
+ /** listening for collection events */
+ _setUpCollectionListeners : function(){
+ _super.prototype._setUpCollectionListeners.call( this );
- this.model.on( 'change:nice_size', this.updateHistoryDiskSize, this );
-
- this.model.contents.on( 'change:deleted', this._handleHdaDeletionChange, this );
- this.model.contents.on( 'change:visible', this._handleHdaVisibleChange, this );
- this.model.contents.on( 'change:purged', function( hda ){
- // hafta get the new nice-size w/o the purged hda
+ this.collection.on( 'change:deleted', this._handleHdaDeletionChange, this );
+ this.collection.on( 'change:visible', this._handleHdaVisibleChange, this );
+ this.collection.on( 'change:purged', function( model ){
+ // hafta get the new nice-size w/o the purged model
this.model.fetch();
}, this );
+ return this;
+ },
+
+ /** listening for history and HDA events */
+ _setUpModelListeners : function(){
+ _super.prototype._setUpModelListeners.call( this );
+ this.model.on( 'change:nice_size', this.updateHistoryDiskSize, this );
+ return this;
},
// ------------------------------------------------------------------------ panel rendering
- /** render with history data
- * In this override, add tags, annotations, and multi select
- * @returns {jQuery} dom fragment as temporary container to be swapped out later
- */
- renderModel : function( ){
-//TODO: can't call ReadOnlyHistoryPanel?
- var $newRender = $( '' );
+ /** In this override, add tag and annotation editors and a btn to toggle the selectors */
+ _buildNewRender : function(){
+ // create a new render using a skeleton template, render title buttons, render body, and set up events, etc.
+ var $newRender = _super.prototype._buildNewRender.call( this );
+ if( !this.model ){ return $newRender; }
- $newRender.append( HistoryPanel.templates.historyPanel( this.model.toJSON() ) );
- this.$emptyMessage( $newRender ).text( this.emptyMsg );
if( Galaxy && Galaxy.currUser && Galaxy.currUser.id && Galaxy.currUser.id === this.model.get( 'user_id' ) ){
this._renderTags( $newRender );
this._renderAnnotation( $newRender );
}
- // search and select available to both anon/logged-in users
- $newRender.find( '.history-secondary-actions' ).prepend( this._renderSelectButton() );
- $newRender.find( '.history-dataset-actions' ).toggle( this.selecting );
- $newRender.find( '.history-secondary-actions' ).prepend( this._renderSearchButton() );
-
- this._setUpBehaviours( $newRender );
-
- // render hda views (if any and any shown (show_deleted/hidden)
- this.renderHdas( $newRender );
return $newRender;
},
@@ -127,9 +109,9 @@ var HistoryPanel = _super.extend(
var panel = this;
this.tagsEditor = new TAGS.TagsEditor({
model : this.model,
- el : $where.find( '.history-controls .tags-display' ),
+ el : $where.find( '.controls .tags-display' ),
onshowFirstTime : function(){ this.render(); },
- // show hide hda view tag editors when this is shown/hidden
+ // show hide sub-view tag editors when this is shown/hidden
onshow : function(){
panel.toggleHDATagEditors( true, panel.fxSpeed );
},
@@ -140,7 +122,7 @@ var HistoryPanel = _super.extend(
title : _l( 'Edit history tags' ),
classes : 'history-tag-btn',
faIcon : 'fa-tags'
- }).appendTo( $where.find( '.history-secondary-actions' ) )
+ }).appendTo( $where.find( '.controls .actions' ) )
});
},
/** render the annotation sub-view controller */
@@ -148,9 +130,9 @@ var HistoryPanel = _super.extend(
var panel = this;
this.annotationEditor = new ANNOTATIONS.AnnotationEditor({
model : this.model,
- el : $where.find( '.history-controls .annotation-display' ),
+ el : $where.find( '.controls .annotation-display' ),
onshowFirstTime : function(){ this.render(); },
- // show hide hda view annotation editors when this is shown/hidden
+ // show hide sub-view view annotation editors when this is shown/hidden
onshow : function(){
panel.toggleHDAAnnotationEditors( true, panel.fxSpeed );
},
@@ -161,33 +143,22 @@ var HistoryPanel = _super.extend(
title : _l( 'Edit history annotation' ),
classes : 'history-annotate-btn',
faIcon : 'fa-comment'
- }).appendTo( $where.find( '.history-secondary-actions' ) )
+ }).appendTo( $where.find( '.controls .actions' ) )
});
},
- /** button for starting select mode */
- _renderSelectButton : function( $where ){
- return faIconButton({
- title : _l( 'Operations on multiple datasets' ),
- classes : 'history-select-btn',
- faIcon : 'fa-check-square-o'
- });
- },
-
- /** Set up HistoryPanel js/widget behaviours
+ /** Set up HistoryPanelEdit js/widget behaviours
* In this override, add the multi select popup menu and make the name editable
*/
- _setUpBehaviours : function( $where ){
+ _setUpBehaviors : function( $where ){
$where = $where || this.$el;
- _super.prototype._setUpBehaviours.call( this, $where );
-
- // anon users shouldn't have access to any of the following
- if( !this.model ){
- return;
- }
+ _super.prototype._setUpBehaviors.call( this, $where );
+ if( !this.model ){ return; }
// set up the pupup for actions available when multi selecting
- this.actionsPopup = this._setUpDatasetActionsPopup( $where );
+ if( this.multiselectActions.length ){
+ this.actionsPopup = new PopupMenu( $where.find( '.list-action-popup-btn' ), this.multiselectActions );
+ }
// anon users shouldn't have access to any of the following
if( ( !Galaxy.currUser || Galaxy.currUser.isAnonymous() )
@@ -195,20 +166,22 @@ var HistoryPanel = _super.extend(
return;
}
- var panel = this;
- $where.find( '.history-name' )
- .attr( 'title', _l( 'Click to rename history' ) ).tooltip({ placement: 'bottom' })
+ var panel = this,
+ nameSelector = '.controls .name';
+ $where.find( nameSelector )
+ .attr( 'title', _l( 'Click to rename history' ) )
+ .tooltip({ placement: 'bottom' })
.make_text_editable({
on_finish: function( newName ){
var previousName = panel.model.get( 'name' );
if( newName && newName !== previousName ){
- panel.$el.find( '.history-name' ).text( newName );
+ panel.$el.find( nameSelector ).text( newName );
panel.model.save({ name: newName })
.fail( function(){
- panel.$el.find( '.history-name' ).text( panel.model.previous( 'name' ) );
+ panel.$el.find( nameSelector ).text( panel.model.previous( 'name' ) );
});
} else {
- panel.$el.find( '.history-name' ).text( previousName );
+ panel.$el.find( nameSelector ).text( previousName );
}
}
});
@@ -217,61 +190,62 @@ var HistoryPanel = _super.extend(
/** return a new popup menu for choosing a multi selection action
* ajax calls made for multiple datasets are queued
*/
- _setUpDatasetActionsPopup : function( $where ){
+ _getActions : function(){
var panel = this,
actions = [
{ html: _l( 'Hide datasets' ), func: function(){
var action = HDA_MODEL.HistoryDatasetAssociation.prototype.hide;
- panel.getSelectedHdaCollection().ajaxQueue( action );
+ panel.getSelectedModels().ajaxQueue( action );
}
},
{ html: _l( 'Unhide datasets' ), func: function(){
var action = HDA_MODEL.HistoryDatasetAssociation.prototype.unhide;
- panel.getSelectedHdaCollection().ajaxQueue( action );
+ panel.getSelectedModels().ajaxQueue( action );
}
},
{ html: _l( 'Delete datasets' ), func: function(){
var action = HDA_MODEL.HistoryDatasetAssociation.prototype['delete'];
- panel.getSelectedHdaCollection().ajaxQueue( action );
+ panel.getSelectedModels().ajaxQueue( action );
}
},
{ html: _l( 'Undelete datasets' ), func: function(){
var action = HDA_MODEL.HistoryDatasetAssociation.prototype.undelete;
- panel.getSelectedHdaCollection().ajaxQueue( action );
+ panel.getSelectedModels().ajaxQueue( action );
}
- } ];
+ }
+ ];
if( panel.purgeAllowed ){
actions.push({
html: _l( 'Permanently delete datasets' ), func: function(){
if( confirm( _l( 'This will permanently remove the data in your datasets. Are you sure?' ) ) ){
var action = HDA_MODEL.HistoryDatasetAssociation.prototype.purge;
- panel.getSelectedHdaCollection().ajaxQueue( action );
+ panel.getSelectedModels().ajaxQueue( action );
}
}
});
}
actions.push( {
- html: _l( 'Build Dataset List (Experimental)' ), func: function() {
- panel.getSelectedHdaCollection().promoteToHistoryDatasetCollection( panel.model, "list" );
+ html: _l( 'Build Dataset List' ), func: function() {
+ panel.getSelectedModels().promoteToHistoryDatasetCollection( panel.model, "list" );
}
});
actions.push( {
// TODO: Only show quick pair if two things selected.
- html: _l( 'Build Dataset Pair (Experimental)' ), func: function() {
- panel.getSelectedHdaCollection().promoteToHistoryDatasetCollection( panel.model, "paired" );
+ html: _l( 'Build Dataset Pair' ), func: function() {
+ panel.getSelectedModels().promoteToHistoryDatasetCollection( panel.model, "paired" );
}
});
actions.push( {
// TODO: Only show quick pair if two things selected.
- html: _l( 'Build List of Dataset Pairs (Experimental)' ),
+ html: _l( 'Build List of Dataset Pairs' ),
func: _.bind( panel._showPairedCollectionModal, panel )
});
- return new PopupMenu( $where.find( '.history-dataset-action-popup-btn' ), actions );
+ return actions;
},
_showPairedCollectionModal : function(){
var panel = this,
- datasets = panel.getSelectedHdaCollection().toJSON().filter( function( content ){
+ datasets = panel.getSelectedModels().toJSON().filter( function( content ){
return content.history_content_type === 'dataset'
&& content.state === STATES.OK;
});
@@ -291,113 +265,53 @@ var HistoryPanel = _super.extend(
}
},
- // ------------------------------------------------------------------------ hda sub-views
- /** If this hda is deleted and we're not showing deleted hdas, remove the view
- * @param {HistoryDataAssociation} the hda to check
- */
- _handleHdaDeletionChange : function( hda ){
- if( hda.get( 'deleted' ) && !this.storage.get( 'show_deleted' ) ){
- this.removeHdaView( this.hdaViews[ hda.id ] );
- } // otherwise, the hdaView rendering should handle it
- },
-
-
- /** If this hda is hidden and we're not showing hidden hdas, remove the view
- * @param {HistoryDataAssociation} the hda to check
- */
- _handleHdaVisibleChange : function( hda ){
- if( hda.hidden() && !this.storage.get( 'show_hidden' ) ){
- this.removeHdaView( this.hdaViews[ hda.id ] );
- } // otherwise, the hdaView rendering should handle it
- },
-
- _getContentOptions : function( content ){
- var options = _super.prototype._getContentOptions.call( this, content );
+ // ------------------------------------------------------------------------ sub-views
+ /** In this override, add purgeAllowed and whether tags/annotation editors should be shown */
+ _getItemViewOptions : function( model ){
+ var options = _super.prototype._getItemViewOptions.call( this, model );
_.extend( options, {
- selectable : this.selecting,
purgeAllowed : this.purgeAllowed,
+//TODO: not working
tagsEditorShown : ( this.tagsEditor && !this.tagsEditor.hidden ),
annotationEditorShown : ( this.annotationEditor && !this.annotationEditor.hidden )
});
return options;
},
- /** Set up HistoryPanel listeners for HDAView events. Currently binds:
- * HDAView#body-visible, HDAView#body-hidden to store expanded states
- * @param {HDAView} hdaView HDAView (base or edit) to listen to
+ /** If this item is deleted and we're not showing deleted items, remove the view
+ * @param {Model} the item model to check
*/
- _setUpHdaListeners : function( hdaView ){
- var historyView = this;
- _super.prototype._setUpHdaListeners.call( this, hdaView );
-
- // maintain a list of hdas that are selected
- hdaView.on( 'selected', function( hdaView, event ){
- if( !event ){ return; }
- //this.debug( 'selected', event );
- var selectedIds = [];
- //this.debug( historyView.lastSelectedViewId, historyView.hdaViews[ historyView.lastSelectedViewId ] );
-
- // shift will select a range, but not set lastSelectedViewId
- if( ( event.shiftKey )
- && ( historyView.lastSelectedViewId && _.has( historyView.hdaViews, historyView.lastSelectedViewId ) ) ){
- var lastSelectedView = historyView.hdaViews[ historyView.lastSelectedViewId ];
- selectedIds = historyView.selectDatasetRange( hdaView, lastSelectedView )
- .map( function( view ){ return view.model.id; });
-
- // only
- } else {
- var id = hdaView.model.id;
- historyView.lastSelectedViewId = id;
- //this.debug( 'lastSelectedViewId:', historyView.lastSelectedViewId );
- selectedIds = [ id ];
- }
- //this.debug( 'selectedIds:', selectedIds );
- historyView.selectedHdaIds = _.union( historyView.selectedHdaIds, selectedIds );
- //TODO: might want to use getSelectedHdaViews instead managing these lists with ops
- //this.debug( 'selected', historyView.selectedHdaIds );
- });
- hdaView.on( 'de-selected', function( hdaView, event ){
- //this.debug( 'de-selected', event );
- var id = hdaView.model.id;
- historyView.selectedHdaIds = _.without( historyView.selectedHdaIds, id );
- //this.debug( 'de-selected', historyView.selectedHdaIds );
- });
+ _handleHdaDeletionChange : function( itemModel ){
+ if( itemModel.get( 'deleted' ) && !this.storage.get( 'show_deleted' ) ){
+ this.removeItemView( itemModel );
+ }
},
- /** toggle the visibility of each hdaView's tagsEditor applying all the args sent to this function */
+ /** If this item is hidden and we're not showing hidden items, remove the view
+ * @param {Model} the item model to check
+ */
+ _handleHdaVisibleChange : function( itemModel ){
+ if( itemModel.hidden() && !this.storage.get( 'show_hidden' ) ){
+ this.removeItemView( itemModel );
+ }
+ },
+
+ /** toggle the visibility of each content's tagsEditor applying all the args sent to this function */
toggleHDATagEditors : function( showOrHide ){
- var args = arguments;
- _.each( this.hdaViews, function( hdaView ){
- if( hdaView.tagsEditor ){
- hdaView.tagsEditor.toggle.apply( hdaView.tagsEditor, args );
+ var args = Array.prototype.slice.call( arguments, 1 );
+ _.each( this.views, function( view ){
+ if( view.tagsEditor ){
+ view.tagsEditor.toggle.apply( view.tagsEditor, args );
}
});
},
- /** toggle the visibility of each hdaView's annotationEditor applying all the args sent to this function */
+ /** toggle the visibility of each content's annotationEditor applying all the args sent to this function */
toggleHDAAnnotationEditors : function( showOrHide ){
- var args = arguments;
- _.each( this.hdaViews, function( hdaView ){
- if( hdaView.annotationEditor ){
- hdaView.annotationEditor.toggle.apply( hdaView.annotationEditor, args );
- }
- });
- },
-
- /** Remove a view from the panel and if the panel is now empty, re-render
- * @param {Int} the id of the hdaView to remove
- */
- removeHdaView : function( hdaView ){
- if( !hdaView ){ return; }
-
- var panel = this;
- hdaView.$el.fadeOut( panel.fxSpeed, function(){
- hdaView.off();
- hdaView.remove();
- delete panel.hdaViews[ hdaView.model.id ];
- if( _.isEmpty( panel.hdaViews ) ){
- panel.trigger( 'empty-history', panel );
- panel._renderEmptyMsg();
+ var args = Array.prototype.slice.call( arguments, 1 );
+ _.each( this.views, function( view ){
+ if( view.annotationEditor ){
+ view.annotationEditor.toggle.apply( view.annotationEditor, args );
}
});
},
@@ -405,9 +319,7 @@ var HistoryPanel = _super.extend(
// ------------------------------------------------------------------------ panel events
/** event map */
events : _.extend( _.clone( _super.prototype.events ), {
- 'click .history-select-btn' : 'toggleSelectors',
- 'click .history-select-all-datasets-btn' : 'selectAllDatasets',
- 'click .history-deselect-all-datasets-btn' : 'deselectAllDatasets'
+ 'click .show-selectors-btn' : 'toggleSelectors'
}),
/** Update the history size display (curr. upper right of panel).
@@ -416,88 +328,15 @@ var HistoryPanel = _super.extend(
this.$el.find( '.history-size' ).text( this.model.get( 'nice_size' ) );
},
- // ........................................................................ multi-select of hdas
-//TODO: to toggle showOrHide pattern
- /** show selectors on all visible hdas and associated controls */
- showSelectors : function( speed ){
- speed = ( speed !== undefined )?( speed ):( this.fxSpeed );
- this.selecting = true;
- this.$( '.history-dataset-actions' ).slideDown( speed );
- _.each( this.hdaViews, function( view ){
- view.showSelector();
- });
- this.selectedHdaIds = [];
- this.lastSelectedViewId = null;
- },
-
- /** hide selectors on all visible hdas and associated controls */
- hideSelectors : function( speed ){
- speed = ( speed !== undefined )?( speed ):( this.fxSpeed );
- this.selecting = false;
- this.$( '.history-dataset-actions' ).slideUp( speed );
- _.each( this.hdaViews, function( view ){
- view.hideSelector();
- });
- this.selectedHdaIds = [];
- this.lastSelectedViewId = null;
- },
-
- /** show or hide selectors on all visible hdas and associated controls */
- toggleSelectors : function(){
- if( !this.selecting ){
- this.showSelectors();
- } else {
- this.hideSelectors();
- }
- },
-
- /** select all visible hdas */
- selectAllDatasets : function( event ){
- _.each( this.hdaViews, function( view ){
- view.select( event );
- });
- },
-
- /** deselect all visible hdas */
- deselectAllDatasets : function( event ){
- this.lastSelectedViewId = null;
- _.each( this.hdaViews, function( view ){
- view.deselect( event );
- });
- },
-
- /** select a range of datasets between A and B */
- selectDatasetRange : function( viewA, viewB ){
- var range = this.hdaViewRange( viewA, viewB );
- _.each( range, function( view ){
- view.select();
- });
- return range;
- },
-
- /** return an array of all currently selected hdas */
- getSelectedHdaViews : function(){
- return _.filter( this.hdaViews, function( v ){
- return v.selected;
- });
- },
-
- /** return an HdaCollection of the models of all currenly selected hdas */
- getSelectedHdaCollection : function(){
- return new HISTORY_CONTENTS.HistoryContents( _.map( this.getSelectedHdaViews(), function( hdaView ){
- return hdaView.model;
- }), { historyId: this.model.id });
- },
-
// ........................................................................ misc
/** Return a string rep of the history */
toString : function(){
- return 'HistoryPanel(' + (( this.model )?( this.model.get( 'name' )):( '' )) + ')';
+ return 'HistoryPanelEdit(' + (( this.model )?( this.model.get( 'name' )):( '' )) + ')';
}
});
//==============================================================================
return {
- HistoryPanel : HistoryPanel
+ HistoryPanelEdit : HistoryPanelEdit
};
});
diff --git a/static/scripts/mvc/history/history-panel.js b/static/scripts/mvc/history/history-panel.js
index e1f0ca195c4..e2482b1e525 100644
--- a/static/scripts/mvc/history/history-panel.js
+++ b/static/scripts/mvc/history/history-panel.js
@@ -1,19 +1,21 @@
define([
+ "mvc/list/list-panel",
"mvc/history/history-model",
+ "mvc/history/history-contents",
"mvc/history/hda-li",
"mvc/history/hdca-li",
"mvc/user/user-model",
"mvc/base-mvc",
"utils/localization"
-], function( HISTORY_MODEL, HDA_LI, HDCA_LI, USER, BASE_MVC, _l ){
+], function( LIST_PANEL, HISTORY_MODEL, HISTORY_CONTENTS, HDA_LI, HDCA_LI, USER, BASE_MVC, _l ){
// ============================================================================
/** session storage for individual history preferences */
var HistoryPrefs = BASE_MVC.SessionStorageModel.extend(
/** @lends HistoryPrefs.prototype */{
-
+//TODO:?? possibly mark as current T/F - have History.currId() (a class method) return that value
defaults : {
-//TODO:?? expandedHdas to array?
- expandedHdas : {},
+//TODO:?? expandedIds to array?
+ expandedIds : {},
//TODO:?? move to user?
show_deleted : false,
show_hidden : false
@@ -21,12 +23,13 @@ var HistoryPrefs = BASE_MVC.SessionStorageModel.extend(
},
/** add an hda id to the hash of expanded hdas */
addExpandedHda : function( model ){
- var key = 'expandedHdas';
+ var key = 'expandedIds';
+//TODO:?? is this right anymore?
this.save( key, _.extend( this.get( key ), _.object([ model.id ], [ model.get( 'id' ) ]) ) );
},
/** remove an hda id from the hash of expanded hdas */
removeExpandedHda : function( model ){
- var key = 'expandedHdas';
+ var key = 'expandedIds';
this.save( key, _.omit( this.get( key ), model.id ) );
},
toString : function(){
@@ -62,137 +65,348 @@ HistoryPrefs.clearAll = function clearAll( historyId ){
TODO:
============================================================================= */
-// base model and used as-is in history/view.mako
/** @class non-editable, read-only View/Controller for a history model.
- * @name HistoryPanel
- *
* Allows:
* changing the loaded history
- * searching hdas
* displaying data, info, and download
+ * tracking history attrs: size, tags, annotations, name, etc.
* Does not allow:
* changing the name
- *
- * @augments Backbone.View
- * @borrows LoggableMixin#logger as #logger
- * @borrows LoggableMixin#log as #log
- * @constructs
*/
-var ReadOnlyHistoryPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
-/** @lends ReadOnlyHistoryPanel.prototype */{
+var _super = LIST_PANEL.ModelListPanel;
+var HistoryPanel = _super.extend(
+/** @lends HistoryPanel.prototype */{
/** logger used to record this.log messages, commonly set to console */
- // comment this out to suppress log output
//logger : console,
/** class to use for constructing the HDA views */
HDAViewClass : HDA_LI.HDAListItemView,
/** class to use for constructing the HDCA views */
HDCAViewClass : HDCA_LI.HDCAListItemView,
+ /** class to used for constructing collection of sub-view models */
+ collectionClass : HISTORY_CONTENTS.HistoryContents,
+ /** key of attribute in model to assign to this.collection */
+ modelCollectionKey : 'contents',
tagName : 'div',
- className : 'history-panel',
+ className : _super.prototype.className + ' history-panel',
- /** (in ms) that jquery effects will use */
- fxSpeed : 'fast',
-
- /** string to display when the model has no hdas */
+ /** string to display when the collection is empty */
emptyMsg : _l( 'This history is empty' ),
- /** string to no hdas match the search terms */
+ /** displayed when no items match the search terms */
noneFoundMsg : _l( 'No matching datasets found' ),
+ /** string used for search placeholder */
+ searchPlaceholder : _l( 'search datasets' ),
// ......................................................................... SET UP
- /** Set up the view, set up storage, bind listeners to HistoryContents events
+ /** Set up the view, bind listeners.
* @param {Object} attributes optional settings for the panel
*/
initialize : function( attributes ){
- attributes = attributes || {};
- // set the logger if requested
- if( attributes.logger ){
- this.logger = attributes.logger;
- }
- this.log( this + '.initialize:', attributes );
-
+ _super.prototype.initialize.call( this, attributes );
// ---- instance vars
// control contents/behavior based on where (and in what context) the panel is being used
/** where should pages from links be displayed? (default to new tab/window) */
this.linkTarget = attributes.linkTarget || '_blank';
- /** how quickly should jquery fx run? */
- this.fxSpeed = _.has( attributes, 'fxSpeed' )?( attributes.fxSpeed ):( this.fxSpeed );
-
- /** filters for displaying hdas */
- this.filters = [];
- this.searchFor = '';
-
- /** a function to locate the container to scroll to effectively scroll the panel */
-//TODO: rename
- this.findContainerFn = attributes.findContainerFn;
- // generally this is $el.parent() - but may be $el or $el.parent().parent() depending on the context
-
- // ---- sub views and saved elements
- /** map of hda model ids to hda views */
- this.hdaViews = {};
- /** loading indicator */
- this.indicator = new LoadingIndicator( this.$el );
-
- // ----- set up panel listeners, handle models passed on init, and call any ready functions
- this._setUpListeners();
- // don't render when setting the first time
- var modelOptions = _.pick( attributes, 'initiallyExpanded', 'show_deleted', 'show_hidden' );
- this.setModel( this.model, modelOptions, false );
-//TODO: remove?
- if( attributes.onready ){
- attributes.onready.call( this );
- }
},
- /** create any event listeners for the panel
- * @fires: rendered:initial on the first render
- * @fires: empty-history when switching to a history with no HDAs or creating a new history
- */
- _setUpListeners : function(){
- this.on( 'error', function( model, xhr, options, msg, details ){
- this.errorHandler( model, xhr, options, msg, details );
- });
-
- this.on( 'loading-history', function(){
- // show the loading indicator when loading a new history starts...
- this._showLoadingIndicator( 'loading history...', 40 );
- });
- this.on( 'loading-done', function(){
- // ...hiding it again when loading is done (or there's been an error)
- this._hideLoadingIndicator( 40 );
- if( _.isEmpty( this.hdaViews ) ){
- this.trigger( 'empty-history', this );
- }
- });
-
- // throw the first render up as a diff namespace using once (for outside consumption)
- this.once( 'rendered', function(){
- this.trigger( 'rendered:initial', this );
- return false;
- });
-
- // debugging
- if( this.logger ){
- this.on( 'all', function( event ){
- this.log( this + '', arguments );
- }, this );
+ /** In this override, clear the update timer on the model */
+ freeModel : function(){
+ _super.prototype.freeModel.call( this );
+//TODO: move to History.free()
+ if( this.model ){
+ this.model.clearUpdateTimeout();
}
return this;
},
- //TODO: see base-mvc
- //onFree : function(){
- // _.each( this.hdaViews, function( view, modelId ){
- // view.free();
- // });
- // this.hdaViews = null;
- //},
+ /** create any event listeners for the panel
+ * @fires: rendered:initial on the first render
+ * @fires: empty-history when switching to a history with no contents or creating a new history
+ */
+ _setUpListeners : function(){
+ _super.prototype._setUpListeners.call( this );
+
+ this.on( 'error', function( model, xhr, options, msg, details ){
+ this.errorHandler( model, xhr, options, msg, details );
+ });
+
+ this.on( 'loading-done', function(){
+ //TODO:?? if( this.collection.length ){
+ if( !this.views.length ){
+ this.trigger( 'empty-history', this );
+ }
+ });
+ },
+
+ // ------------------------------------------------------------------------ loading history/hda models
+ //NOTE: all the following fns replace the existing history model with a new model
+ // (in the following 'details' refers to the full set of contents api data (urls, display_apps, misc_info, etc.)
+ // - contents w/o details will have summary data only (name, hid, deleted, visible, state, etc.))
+//TODO: too tangled...
+
+ /** loads a history & contents, getting details of any contents whose ids are stored in sessionStorage
+ * (but does not make them the current history)
+ */
+ loadHistoryWithDetails : function( historyId, attributes, historyFn, contentsFn ){
+ this.info( 'loadHistoryWithDetails:', historyId, attributes, historyFn, contentsFn );
+ var detailIdsFn = function( historyData ){
+ // will be called to get content ids that need details from the api
+//TODO:! non-visible contents are getting details loaded... either stop loading them at all or filter ids thru isVisible
+ return _.values( HistoryPrefs.get( historyData.id ).get( 'expandedIds' ) );
+ };
+ return this.loadHistory( historyId, attributes, historyFn, contentsFn, detailIdsFn );
+ },
+
+ /** loads a history & contents (but does not make them the current history) */
+ loadHistory : function( historyId, attributes, historyFn, contentsFn, detailIdsFn ){
+ this.info( 'loadHistory:', historyId, attributes, historyFn, contentsFn, detailIdsFn );
+ var panel = this;
+ attributes = attributes || {};
+
+ panel.trigger( 'loading', panel );
+ //this.info( 'loadHistory:', historyId, attributes, historyFn, contentsFn, detailIdsFn );
+ var xhr = HISTORY_MODEL.History.getHistoryData( historyId, {
+ historyFn : historyFn,
+ contentsFn : contentsFn,
+ detailIdsFn : attributes.initiallyExpanded || detailIdsFn
+ });
+
+ return panel._loadHistoryFromXHR( xhr, attributes )
+ .fail( function( xhr, where, history ){
+ // throw an error up for the error handler
+ panel.trigger( 'error', panel, xhr, attributes, _l( 'An error was encountered while ' + where ),
+ { historyId: historyId, history: history || {} });
+ })
+ .always( function(){
+ // bc _hideLoadingIndicator relies on this firing
+ panel.trigger( 'loading-done', panel );
+ });
+ },
+
+ /** given an xhr that will provide both history and contents data, pass data to set model or handle xhr errors */
+ _loadHistoryFromXHR : function( xhr, attributes ){
+ var panel = this;
+ xhr.then( function( historyJSON, contentsJSON ){
+ panel.JSONToModel( historyJSON, contentsJSON, attributes );
+ panel.render();
+ });
+ xhr.fail( function( xhr, where ){
+ // render anyways - whether we get a model or not
+ panel.render();
+ });
+ return xhr;
+ },
+
+//TODO:?? seems unneccesary
+//TODO: Maybe better in History?
+ /** create a new history model from JSON and call setModel on it */
+ JSONToModel : function( newHistoryJSON, newHdaJSON, attributes ){
+ this.log( 'JSONToModel:', newHistoryJSON, newHdaJSON, attributes );
+ attributes = attributes || {};
+ //this.log( 'JSONToModel:', newHistoryJSON, newHdaJSON.length, attributes );
+
+ var model = new HISTORY_MODEL.History( newHistoryJSON, newHdaJSON, attributes );
+//TODO:?? here?
+ this.setModel( model );
+ return model;
+ },
+
+ /** release/free/shutdown old models and set up panel for new models
+ * @fires new-model with the panel as parameter
+ */
+ setModel : function( model, attributes ){
+ attributes = attributes || {};
+ _super.prototype.setModel.call( this, model, attributes );
+ if( this.model ){
+ this._setUpWebStorage( attributes.initiallyExpanded, attributes.show_deleted, attributes.show_hidden );
+ }
+ },
+
+ // ------------------------------------------------------------------------ browser stored prefs
+ /** Set up client side storage. Currently PersistanStorage keyed under 'HistoryPanel.'
+ * @param {Object} initiallyExpanded
+ * @param {Boolean} show_deleted whether to show deleted contents (overrides stored)
+ * @param {Boolean} show_hidden
+ * @see PersistentStorage
+ */
+ _setUpWebStorage : function( initiallyExpanded, show_deleted, show_hidden ){
+ //if( !this.model ){ return this; }
+ //this.log( '_setUpWebStorage', initiallyExpanded, show_deleted, show_hidden );
+ this.storage = new HistoryPrefs({
+ id: HistoryPrefs.historyStorageKey( this.model.get( 'id' ) )
+ });
+
+ // expandedIds is a map of content.ids -> a boolean repr'ing whether that item's body is already expanded
+ // store any pre-expanded ids passed in
+ if( _.isObject( initiallyExpanded ) ){
+ this.storage.set( 'expandedIds', initiallyExpanded );
+ }
+
+ // get the show_deleted/hidden settings giving priority to values passed in, using web storage otherwise
+ // if the page has specifically requested show_deleted/hidden, these will be either true or false
+ // (as opposed to undefined, null) - and we give priority to that setting
+ if( _.isBoolean( show_deleted ) ){
+ this.storage.set( 'show_deleted', show_deleted );
+ }
+ if( _.isBoolean( show_hidden ) ){
+ this.storage.set( 'show_hidden', show_hidden );
+ }
+
+ this.trigger( 'new-storage', this.storage, this );
+ this.log( this + ' (init\'d) storage:', this.storage.get() );
+ return this;
+ },
+
+ // ------------------------------------------------------------------------ panel rendering
+ /** In this override, add a btn to toggle the selectors */
+ _buildNewRender : function(){
+ var $newRender = _super.prototype._buildNewRender.call( this );
+ if( this.multiselectActions.length ){
+ $newRender.find( '.controls .actions' ).prepend( this._renderSelectButton() );
+ }
+ return $newRender;
+ },
+
+ /** button for starting select mode */
+ _renderSelectButton : function( $where ){
+ return faIconButton({
+ title : _l( 'Operations on multiple datasets' ),
+ classes : 'show-selectors-btn',
+ faIcon : 'fa-check-square-o'
+ });
+ },
+
+ // ------------------------------------------------------------------------ sub-views
+ /** In this override, since history contents are mixed,
+ * get the appropo view class based on history_content_type
+ */
+ _getItemViewClass : function( model ){
+ var contentType = model.get( "history_content_type" );
+ switch( contentType ){
+ case 'dataset':
+ return this.HDAViewClass;
+ case 'dataset_collection':
+ return this.HDCAViewClass;
+ }
+ throw new TypeError( 'Unknown history_content_type: ' + contentType );
+ },
+
+ /** in this override, check if the contents would also display based on show_deleted/hidden */
+ _filterItem : function( model ){
+ var panel = this;
+ return ( _super.prototype._filterItem.call( panel, model )
+ && ( !model.hidden() || panel.storage.get( 'show_hidden' ) )
+ && ( !model.isDeletedOrPurged() || panel.storage.get( 'show_deleted' ) ) );
+ },
+
+ /** in this override, add a linktarget, and expand if id is in web storage */
+ _getItemViewOptions : function( model ){
+ var options = _super.prototype._getItemViewOptions.call( this, model );
+ return _.extend( options, {
+ linkTarget : this.linkTarget,
+ expanded : !!this.storage.get( 'expandedIds' )[ model.id ],
+ hasUser : this.model.ownedByCurrUser()
+ });
+ },
+
+ /** In this override, add/remove expanded/collapsed model ids to/from web storage */
+ _setUpItemViewListeners : function( view ){
+ var panel = this;
+ _super.prototype._setUpItemViewListeners.call( panel, view );
+
+ //TODO:?? could use 'view:expanded' here?
+ // maintain a list of items whose bodies are expanded
+ view.on( 'expanded', function( v ){
+ panel.storage.addExpandedHda( v.model );
+ });
+ view.on( 'collapsed', function( v ){
+ panel.storage.removeExpandedHda( v.model );
+ });
+ return this;
+ },
+
+ // ------------------------------------------------------------------------ collection/views syncing
+ /** convenience alias to the model. Updates the item list only (not the history) */
+ refreshContents : function( detailIds, options ){
+ if( this.model ){
+ return this.model.refresh( detailIds, options );
+ }
+ // may have callbacks - so return an empty promise
+ return $.when();
+ },
+
+ // ------------------------------------------------------------------------ selection
+ /** Override to correctly set the historyId of the new collection */
+ getSelectedModels : function(){
+ var collection = _super.prototype.getSelectedModels.call( this );
+ collection.historyId = this.collection.historyId;
+ return collection;
+ },
+
+ // ------------------------------------------------------------------------ panel events
+ /** event map */
+ events : _.extend( _.clone( _super.prototype.events ), {
+ // toggle list item selectors
+ 'click .show-selectors-btn' : 'toggleSelectors'
+ // allow (error) messages to be clicked away
+//TODO: switch to common close (X) idiom
+ //'click .messages' : 'clearMessages',
+//TODO: remove
+ //'click .history-search-btn' : 'toggleSearchControls'
+ }),
+
+ /** Handle the user toggling the deleted visibility by:
+ * (1) storing the new value in the persistent storage
+ * (2) re-rendering the history
+ * @returns {Boolean} new show_deleted setting
+ */
+ toggleShowDeleted : function( show ){
+ show = ( show !== undefined )?( show ):( !this.storage.get( 'show_deleted' ) );
+ this.storage.set( 'show_deleted', show );
+ //TODO:?? to events on storage('change:show_deleted')
+ this.renderItems();
+ return this.storage.get( 'show_deleted' );
+ },
+
+ /** Handle the user toggling the deleted visibility by:
+ * (1) storing the new value in the persistent storage
+ * (2) re-rendering the history
+ * @returns {Boolean} new show_hidden setting
+ */
+ toggleShowHidden : function( show ){
+ show = ( show !== undefined )?( show ):( !this.storage.get( 'show_hidden' ) );
+ this.storage.set( 'show_hidden', show );
+ //TODO:?? to events on storage('change:show_hidden')
+ this.renderItems();
+ return this.storage.get( 'show_hidden' );
+ },
+
+ /** On the first search, if there are no details - load them, then search */
+ _firstSearch : function( searchFor ){
+ var panel = this,
+ inputSelector = '.history-search-input';
+ this.log( 'onFirstSearch', searchFor );
+
+ if( panel.model.contents.haveDetails() ){
+ panel.searchItems( searchFor );
+ return;
+ }
+
+ panel.$el.find( inputSelector ).searchInput( 'toggle-loading' );
+ panel.model.contents.fetchAllDetails({ silent: true })
+ .always( function(){
+ panel.$el.find( inputSelector ).searchInput( 'toggle-loading' );
+ })
+ .done( function(){
+ panel.searchItems( searchFor );
+ });
+ },
//TODO: break this out
// ........................................................................ error handling
- /** Event handler for errors (from the panel, the history, or the history's HDAs)
+ /** Event handler for errors (from the panel, the history, or the history's contents)
* @param {Model or View} model the (Backbone) source of the error
* @param {XMLHTTPRequest} xhr any ajax obj. assoc. with the error
* @param {Object} options the options map commonly used with bbone ajax
@@ -203,8 +417,6 @@ var ReadOnlyHistoryPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend
this.error( model, xhr, options, msg, details );
//TODO: getting JSON parse errors from jq migrate
- window.xhr = xhr;
-
// interrupted ajax
if( xhr && xhr.status === 0 && xhr.readyState === 0 ){
@@ -231,13 +443,17 @@ var ReadOnlyHistoryPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend
* note: see errorHandler for more info on params
*/
_parseErrorMessage : function( model, xhr, options, msg, details, sentryId ){
+ //if( xhr.responseText ){
+ // xhr.responseText = _.escape( xhr.responseText );
+ //}
var user = Galaxy.currUser,
// add the args (w/ some extra info) into an obj
parsed = {
message : this._bePolite( msg ),
details : {
message : msg,
- raven : ( window.Raven )?( Raven.lastEventId() ):( undefined ),
+ raven : ( window.Raven && _.isFunction( Raven.lastEventId) )?
+ ( Raven.lastEventId() ):( undefined ),
agent : navigator.userAgent,
// add ajax data from Galaxy object cache
url : ( window.Galaxy )?( Galaxy.lastAjax.url ):( undefined ),
@@ -269,670 +485,6 @@ var ReadOnlyHistoryPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend
return msg + '. ' + _l( 'Please contact a Galaxy administrator if the problem persists' ) + '.';
},
- // ------------------------------------------------------------------------ loading history/hda models
- //NOTE: all the following fns replace the existing history model with a new model
- // (in the following 'details' refers to the full set of hda api data (urls, display_apps, misc_info, etc.)
- // - hdas w/o details will have summary data only (name, hid, deleted, visible, state, etc.))
-//TODO: too tangled...
-
- /** loads a history & hdas w/ details (but does not make them the current history) */
- loadHistoryWithHDADetails : function( historyId, attributes, historyFn, hdaFn ){
- this.info( 'loadHistoryWithHDADetails:', historyId, attributes, historyFn, hdaFn );
- var hdaDetailIds = function( historyData ){
- // will be called to get hda ids that need details from the api
-//TODO: non-visible HDAs are getting details loaded... either stop loading them at all or filter ids thru isVisible
- return _.values( HistoryPrefs.get( historyData.id ).get( 'expandedHdas' ) );
- };
- return this.loadHistory( historyId, attributes, historyFn, hdaFn, hdaDetailIds );
- },
-
- /** loads a history & hdas w/ NO details (but does not make them the current history) */
- loadHistory : function( historyId, attributes, historyFn, hdaFn, hdaDetailIds ){
- this.info( 'loadHistory:', historyId, attributes, historyFn, hdaFn, hdaDetailIds );
- var panel = this;
- attributes = attributes || {};
-
- panel.trigger( 'loading-history', panel );
- //this.info( 'loadHistory:', historyId, attributes, historyFn, hdaFn, hdaDetailIds );
- var xhr = HISTORY_MODEL.History.getHistoryData( historyId, {
- historyFn : historyFn,
- hdaFn : hdaFn,
- hdaDetailIds : attributes.initiallyExpanded || hdaDetailIds
- });
-
- return panel._loadHistoryFromXHR( xhr, attributes )
- .fail( function( xhr, where, history ){
- // throw an error up for the error handler
-//TODO: difficult to localize - use template
- panel.trigger( 'error', panel, xhr, attributes, _l( 'An error was encountered while ' + where ),
- { historyId: historyId, history: history || {} });
- })
- .always( function(){
- // bc _hideLoadingIndicator relies on this firing
- panel.trigger( 'loading-done', panel );
- });
- },
-
- /** given an xhr that will provide both history and hda data, pass data to set model or handle xhr errors */
- _loadHistoryFromXHR : function( xhr, attributes ){
- var panel = this;
- xhr.then( function( historyJSON, hdaJSON ){
- panel.JSONToModel( historyJSON, hdaJSON, attributes );
- });
- xhr.fail( function( xhr, where ){
- // always render - whether we get a model or not
- panel.render();
- });
- return xhr;
- },
-
-
- /** create a new history model from JSON and call setModel on it */
- JSONToModel : function( newHistoryJSON, newHdaJSON, attributes ){
- this.log( 'JSONToModel:', newHistoryJSON, newHdaJSON, attributes );
-//TODO: Maybe better in History?
- attributes = attributes || {};
- //this.log( 'JSONToModel:', newHistoryJSON, newHdaJSON.length, attributes );
-
-// // set up the new model and render
-// if( Galaxy && Galaxy.currUser ){
-////TODO: global
-// newHistoryJSON.user = Galaxy.currUser.toJSON();
-// }
- var model = new HISTORY_MODEL.History( newHistoryJSON, newHdaJSON, attributes );
- this.setModel( model );
- return this;
- },
-
- /** release/free/shutdown old models and set up panel for new models
- * @fires new-model with the panel as parameter
- */
- setModel : function( model, attributes, render ){
- attributes = attributes || {};
- render = ( render !== undefined )?( render ):( true );
- this.log( 'setModel:', model, attributes, render );
-
- this.freeModel();
- this.selectedHdaIds = [];
-
- if( model ){
- // set up the new model with user, logger, storage, events
-// if( Galaxy && Galaxy.currUser ){
-////TODO: global
-// model.user = Galaxy.currUser.toJSON();
-// }
- this.model = model;
- if( this.logger ){
- this.model.logger = this.logger;
- }
- this._setUpWebStorage( attributes.initiallyExpanded, attributes.show_deleted, attributes.show_hidden );
- this._setUpModelEventHandlers();
- this.trigger( 'new-model', this );
- }
-
- if( render ){
-//TODO: remove?
- this.render();
- }
- return this;
- },
-
- /** free the current model and all listeners for it, free any hdaViews for the model */
- freeModel : function(){
- // stop/release the previous model, and clear cache to hda sub-views
- if( this.model ){
- this.model.clearUpdateTimeout();
- this.stopListening( this.model );
- this.stopListening( this.model.contents );
- //TODO: see base-mvc
- //this.model.free();
- }
- this.freeHdaViews();
- return this;
- },
-
- /** free any hdaViews the panel has */
- freeHdaViews : function(){
- this.hdaViews = {};
- return this;
- },
-
- // ------------------------------------------------------------------------ browser stored prefs
- /** Set up client side storage. Currently PersistanStorage keyed under 'HistoryPanel.'
- * @param {Object} initiallyExpanded
- * @param {Boolean} show_deleted whether to show deleted HDAs (overrides stored)
- * @param {Boolean} show_hidden
- * @see PersistentStorage
- */
- _setUpWebStorage : function( initiallyExpanded, show_deleted, show_hidden ){
- //this.log( '_setUpWebStorage', initiallyExpanded, show_deleted, show_hidden );
- this.storage = new HistoryPrefs({
- id: HistoryPrefs.historyStorageKey( this.model.get( 'id' ) )
- });
-
- // expanded Hdas is a map of hda.ids -> a boolean repr'ing whether this hda's body is already expanded
- // store any pre-expanded ids passed in
- if( _.isObject( initiallyExpanded ) ){
- this.storage.set( 'exandedHdas', initiallyExpanded );
- }
-
- // get the show_deleted/hidden settings giving priority to values passed in, using web storage otherwise
- // if the page has specifically requested show_deleted/hidden, these will be either true or false
- // (as opposed to undefined, null) - and we give priority to that setting
- if( _.isBoolean( show_deleted ) ){
- this.storage.set( 'show_deleted', show_deleted );
- }
- if( _.isBoolean( show_hidden ) ){
- this.storage.set( 'show_hidden', show_hidden );
- }
-
- this.trigger( 'new-storage', this.storage, this );
- this.log( this + ' (init\'d) storage:', this.storage.get() );
- return this;
- },
-
- // ------------------------------------------------------------------------ history/hda event listening
- /** listening for history and HDA events */
- _setUpModelEventHandlers : function(){
- // ---- hdas
- // bind events from the model's hda collection
- // note: don't listen to the hdas for errors, history will pass that to us
- //this.model.contents.on( 'reset', this.addAll, this );
- this.model.contents.on( 'add', this.addContentView, this );
-
- // on a model error - bounce it up to the panel and remove it from the model
- this.model.on( 'error error:contents', function( model, xhr, options, msg ){
- this.errorHandler( model, xhr, options, msg );
- }, this );
- return this;
- },
-
- // ------------------------------------------------------------------------ panel rendering
- /** Render urls, historyPanel body, and hdas (if any are shown)
- * @fires: rendered when the panel is attached and fully visible
- * @see Backbone.View#render
- */
- render : function( speed, callback ){
- this.log( 'render:', speed, callback );
- // send a speed of 0 to have no fade in/out performed
- speed = ( speed === undefined )?( this.fxSpeed ):( speed );
- var panel = this,
- $newRender;
-
- // handle the possibility of no model (can occur if fetching the model returns an error)
- if( this.model ){
- $newRender = this.renderModel();
- } else {
- $newRender = this.renderWithoutModel();
- }
-
- // fade out existing, swap with the new, fade in, set up behaviours
- $( panel ).queue( 'fx', [
- function( next ){
- if( speed && panel.$el.is( ':visible' ) ){
- panel.$el.fadeOut( speed, next );
- } else {
- next();
- }
- },
- function( next ){
- // swap over from temp div newRender
- panel.$el.empty();
- if( $newRender ){
- panel.$el.append( $newRender.children() );
- }
- next();
- },
- function( next ){
- if( speed && !panel.$el.is( ':visible' ) ){
- panel.$el.fadeIn( speed, next );
- } else {
- next();
- }
- },
- function( next ){
- //TODO: ideally, these would be set up before the fade in (can't because of async save text)
- if( callback ){ callback.call( this ); }
- panel.trigger( 'rendered', this );
- next();
- }
- ]);
- return this;
- },
-
- /** render without history data
- * @returns {jQuery} dom fragment with message container only
- */
- renderWithoutModel : function( ){
- // we'll always need the message container
- var $newRender = $( '' ),
- $msgContainer = $( '' ).addClass( 'message-container' )
- .css({ 'margin': '4px' });
- return $newRender.append( $msgContainer );
- },
-
- /** render with history data
- * @returns {jQuery} dom fragment as temporary container to be swapped out later
- */
- renderModel : function( ){
- // tmp div for final swap in render
- var $newRender = $( '' );
-
- // render based on anonymity, set up behaviors
- $newRender.append( ReadOnlyHistoryPanel.templates.historyPanel( this.model.toJSON() ) );
- this.$emptyMessage( $newRender ).text( this.emptyMsg );
- // search and select available to both anon/logged-in users
- $newRender.find( '.history-secondary-actions' ).prepend( this._renderSearchButton() );
-
- this._setUpBehaviours( $newRender );
-
- // render hda views (if any and any shown (show_deleted/hidden)
- this.renderHdas( $newRender );
- return $newRender;
- },
-
- /** render the empty/none-found message */
- _renderEmptyMsg : function( $whereTo ){
- var panel = this,
- $emptyMsg = panel.$emptyMessage( $whereTo );
-
- if( !_.isEmpty( panel.hdaViews ) ){
- $emptyMsg.hide();
-
- } else if( panel.searchFor ){
- $emptyMsg.text( panel.noneFoundMsg ).show();
-
- } else {
- $emptyMsg.text( panel.emptyMsg ).show();
- }
- return this;
- },
-
- /** button for opening search */
- _renderSearchButton : function( $where ){
- return faIconButton({
- title : _l( 'Search datasets' ),
- classes : 'history-search-btn',
- faIcon : 'fa-search'
- });
- },
-
- /** Set up HistoryPanel js/widget behaviours */
- _setUpBehaviours : function( $where ){
- //TODO: these should be either sub-MVs, or handled by events
- $where = $where || this.$el;
- $where.find( '[title]' ).tooltip({ placement: 'bottom' });
- this._setUpSearchInput( $where.find( '.history-search-controls .history-search-input' ) );
- return this;
- },
-
- // ------------------------------------------------------------------------ sub-$element shortcuts
- /** the scroll container for this panel - can be $el, $el.parent(), or grandparent depending on context */
- $container : function(){
- return ( this.findContainerFn )?( this.findContainerFn.call( this ) ):( this.$el.parent() );
- },
- /** where hdaViews are attached */
- $datasetsList : function( $where ){
- return ( $where || this.$el ).find( '.datasets-list' );
- },
- /** container where panel messages are attached */
- $messages : function( $where ){
- return ( $where || this.$el ).find( '.message-container' );
- },
- /** the message displayed when no hdaViews can be shown (no hdas, none matching search) */
- $emptyMessage : function( $where ){
- return ( $where || this.$el ).find( '.empty-history-message' );
- },
-
- // ------------------------------------------------------------------------ hda sub-views
- /** Set up/render a view for each HDA to be shown, init with model and listeners.
- * HDA views are cached to the map this.hdaViews (using the model.id as key).
- * @param {jQuery} $whereTo what dom element to prepend the HDA views to
- * @returns the number of visible hda views
- */
- renderHdas : function( $whereTo ){
- $whereTo = $whereTo || this.$el;
- var panel = this,
- newHdaViews = {},
- // only render the shown hdas
- //TODO: switch to more general filtered pattern
- visibleHdas = this.model.contents.getVisible(
- this.storage.get( 'show_deleted' ),
- this.storage.get( 'show_hidden' ),
- this.filters
- );
- //this.log( 'renderHdas, visibleHdas:', visibleHdas, $whereTo );
-//TODO: prepend to sep div, add as one
-
- this.$datasetsList( $whereTo ).empty();
-
- if( visibleHdas.length ){
- visibleHdas.each( function( hda ){
- // render it (NOTE: reverse order, newest on top (prepend))
- var hdaId = hda.id,
- hdaView = panel._createContentView( hda );
- newHdaViews[ hdaId ] = hdaView;
- // persist selection
- if( _.contains( panel.selectedHdaIds, hdaId ) ){
- hdaView.selected = true;
- }
- panel.attachContentView( hdaView.render(), $whereTo );
- });
- }
- this.hdaViews = newHdaViews;
- this._renderEmptyMsg( $whereTo );
- return this.hdaViews;
- },
-
- /** Create an HDA view for the given HDA and set up listeners (but leave attachment for addHdaView)
- * @param {HistoryDatasetAssociation} hda
- */
- _createContentView : function( content ){
- var ContentClass = this._getContentClass( content ),
- options = _.extend( this._getContentOptions( content ), {
- model : content
- }),
- contentView = new ContentClass( options );
- this._setUpHdaListeners( contentView );
- return contentView;
- },
-
- _getContentClass : function( content ){
- var contentType = content.get( "history_content_type" );
- switch( contentType ){
- case 'dataset':
- return this.HDAViewClass;
- case 'dataset_collection':
- return this.HDCAViewClass;
- }
- throw new TypeError( 'Unknown history_content_type: ' + contentType );
- },
-
- _getContentOptions : function( content ){
- return {
- linkTarget : this.linkTarget,
- expanded : !!this.storage.get( 'expandedHdas' )[ content.id ],
- //draggable : true,
- hasUser : this.model.ownedByCurrUser(),
- logger : this.logger
- };
- },
-
- /** Set up HistoryPanel listeners for HDAView events. Currently binds:
- * HDAView#body-visible, HDAView#body-hidden to store expanded states
- * @param {HDAView} hdaView HDAView (base or edit) to listen to
- */
- _setUpHdaListeners : function( hdaView ){
- var panel = this;
- hdaView.on( 'error', function( model, xhr, options, msg ){
- panel.errorHandler( model, xhr, options, msg );
- });
- // maintain a list of hdas whose bodies are expanded
- hdaView.on( 'expanded', function( view ){
- panel.storage.addExpandedHda( view.model );
- });
- hdaView.on( 'collapsed', function( view ){
- panel.storage.removeExpandedHda( view.model );
- });
- return this;
- },
-
- /** attach an hdaView to the panel */
- attachContentView : function( hdaView, $whereTo ){
- $whereTo = $whereTo || this.$el;
- var $datasetsList = this.$datasetsList( $whereTo );
- $datasetsList.prepend( hdaView.$el );
- return this;
- },
-
- /** Add an hda view to the panel for the given hda
- * @param {HistoryDatasetAssociation} hda
- */
- addContentView : function( hda ){
- this.log( 'add.' + this, hda );
- var panel = this;
-
- // don't add the view if it wouldn't be visible accrd. to current settings
- if( !hda.isVisible( this.storage.get( 'show_deleted' ), this.storage.get( 'show_hidden' ) ) ){
- return panel;
- }
-
- // create and prepend to current el, if it was empty fadeout the emptyMsg first
- $({}).queue([
- function fadeOutEmptyMsg( next ){
- var $emptyMsg = panel.$emptyMessage();
- if( $emptyMsg.is( ':visible' ) ){
- $emptyMsg.fadeOut( panel.fxSpeed, next );
- } else {
- next();
- }
- },
- function createAndPrepend( next ){
- var hdaView = panel._createContentView( hda );
- panel.hdaViews[ hda.id ] = hdaView;
- hdaView.render().$el.hide();
- panel.scrollToTop();
- panel.attachContentView( hdaView );
- hdaView.$el.slideDown( panel.fxSpeed );
- }
- ]);
- return panel;
- },
-
-//TODO: removeHdaView?
- /** Set up/render a view for each HDA to be shown, init with model and listeners.
- * HDA views are cached to the map this.hdaViews (using the model.id as key).
- * @param {jQuery} $whereTo what dom element to prepend the HDA views to
- * @returns the number of visible hda views
- */
- views : function( at ){
- var panel = this,
- visibleHdas = this.model.contents.getVisible(
- this.storage.get( 'show_deleted' ),
- this.storage.get( 'show_hidden' ),
- this.filters
- );
- if( at !== undefined ){
- return panel.hdaViews[ visibleHdas.at( at ).id ];
- }
- return visibleHdas.map( function( hda ){
- return panel.hdaViews[ hda.id ];
- });
- },
-
- /** convenience alias to the model. Updates the hda list only (not the history) */
- refreshContents : function( detailIds, options ){
- if( this.model ){
- return this.model.refresh( detailIds, options );
- }
- // may have callbacks - so return an empty promise
- return $.when();
- },
-
- ///** use underscore's findWhere to find a view where the model matches the terms
- // * note: finds and returns the _first_ matching
- // */
- //findHdaView : function( terms ){
- // if( !this.model || !this.model.contents.length ){ return undefined; }
- // var model = this.model.contents.findWhere( terms );
- // return ( model )?( this.hdaViews[ model.id ] ):( undefined );
- //},
-
- hdaViewRange : function( viewA, viewB ){
- if( viewA === viewB ){ return [ viewA ]; }
- //TODO: would probably be better if we cache'd the views as an ordered list (as well as a map)
- var panel = this,
- withinSet = false,
- set = [];
- this.model.contents.getVisible(
- this.storage.get( 'show_deleted' ),
- this.storage.get( 'show_hidden' ),
- this.filters
- ).each( function( hda ){
- //panel.debug( 'checking: ', hda.get( 'name' ) );
- if( withinSet ){
- //panel.debug( '\t\t adding: ', hda.get( 'name' ) );
- set.push( panel.hdaViews[ hda.id ] );
- if( hda === viewA.model || hda === viewB.model ){
- //panel.debug( '\t found last: ', hda.get( 'name' ) );
- withinSet = false;
- }
- } else {
- if( hda === viewA.model || hda === viewB.model ){
- //panel.debug( 'found first: ', hda.get( 'name' ) );
- withinSet = true;
- set.push( panel.hdaViews[ hda.id ] );
- }
- }
- });
- return set;
- },
-
- // ------------------------------------------------------------------------ panel events
- /** event map */
- events : {
- // allow (error) messages to be clicked away
-//TODO: switch to common close (X) idiom
- 'click .message-container' : 'clearMessages',
- 'click .history-search-btn' : 'toggleSearchControls'
- },
-
- /** Collapse all hda bodies and clear expandedHdas in the storage */
- collapseAllHdaBodies : function(){
- _.each( this.hdaViews, function( item ){
- item.collapse();
- });
- this.storage.set( 'expandedHdas', {} );
- return this;
- },
-
- /** Handle the user toggling the deleted visibility by:
- * (1) storing the new value in the persistent storage
- * (2) re-rendering the history
- * @returns {Boolean} new show_deleted setting
- */
- toggleShowDeleted : function( show ){
- show = ( show !== undefined )?( show ):( !this.storage.get( 'show_deleted' ) );
- this.storage.set( 'show_deleted', show );
- this.renderHdas();
- return this.storage.get( 'show_deleted' );
- },
-
- /** Handle the user toggling the deleted visibility by:
- * (1) storing the new value in the persistent storage
- * (2) re-rendering the history
- * @returns {Boolean} new show_hidden setting
- */
- toggleShowHidden : function( show ){
- show = ( show !== undefined )?( show ):( !this.storage.get( 'show_hidden' ) );
- this.storage.set( 'show_hidden', show );
- this.renderHdas();
- return this.storage.get( 'show_hidden' );
- },
-
- // ........................................................................ hda search & filters
- /** render a search input for filtering datasets shown
- * (see the search section in the HDA model for implementation of the actual searching)
- * return will start the search
- * esc will clear the search
- * clicking the clear button will clear the search
- * uses searchInput in ui.js
- */
- _setUpSearchInput : function( $where ){
- var panel = this,
- inputSelector = '.history-search-input';
-
- function onFirstSearch( searchFor ){
- //this.log( 'onFirstSearch', searchFor, panel );
- if( panel.model.contents.haveDetails() ){
- panel.searchHdas( searchFor );
- return;
- }
- panel.$el.find( inputSelector ).searchInput( 'toggle-loading' );
- panel.model.contents.fetchAllDetails({ silent: true })
- .always( function(){
- panel.$el.find( inputSelector ).searchInput( 'toggle-loading' );
- })
- .done( function(){
- panel.searchHdas( searchFor );
- });
- }
- $where.searchInput({
- initialVal : panel.searchFor,
- name : 'history-search',
- placeholder : 'search datasets',
- classes : 'history-search',
- onfirstsearch : onFirstSearch,
- onsearch : _.bind( this.searchHdas, this ),
- onclear : _.bind( this.clearHdaSearch, this )
- });
- return $where;
- },
- /** toggle showing/hiding the search controls (rendering first on the initial show)
- * @param {Event or Number} eventOrSpeed variadic - if number the speed of the show/hide effect
- * @param {boolean} show force show/hide with T/F
- */
- toggleSearchControls : function( eventOrSpeed, show ){
- var $searchControls = this.$el.find( '.history-search-controls' ),
- speed = ( jQuery.type( eventOrSpeed ) === 'number' )?( eventOrSpeed ):( this.fxSpeed );
- show = ( show !== undefined )?( show ):( !$searchControls.is( ':visible' ) );
- if( show ){
- $searchControls.slideDown( speed, function(){
- $( this ).find( 'input' ).focus();
- });
- } else {
- $searchControls.slideUp( speed );
- }
- return show;
- },
-
- /** filter hda view list to those that contain the searchFor terms
- * (see the search section in the HDA model for implementation of the actual searching)
- */
- searchHdas : function( searchFor ){
- //note: assumes hda details are loaded
- //this.log( 'onSearch', searchFor, this );
- var panel = this;
- this.searchFor = searchFor;
- this.filters = [ function( hda ){ return hda.matchesAll( panel.searchFor ); } ];
- this.trigger( 'search:searching', searchFor, this );
- this.renderHdas();
- return this;
- },
-
- /** clear the search filters and show all views that are normally shown */
- clearHdaSearch : function( searchFor ){
- //this.log( 'onSearchClear', this );
- this.searchFor = '';
- this.filters = [];
- this.trigger( 'search:clear', this );
- this.renderHdas();
- return this;
- },
-
- // ........................................................................ loading indicator
- /** hide the panel and display a loading indicator (in the panel's parent) when history model's are switched */
- _showLoadingIndicator : function( msg, speed, callback ){
- speed = ( speed !== undefined )?( speed ):( this.fxSpeed );
- if( !this.indicator ){
- this.indicator = new LoadingIndicator( this.$el, this.$el.parent() );
- }
- if( !this.$el.is( ':visible' ) ){
- this.indicator.show( 0, callback );
- } else {
- this.$el.fadeOut( speed );
- this.indicator.show( msg, speed, callback );
- }
- },
-
- /** hide the loading indicator */
- _hideLoadingIndicator : function( speed, callback ){
- speed = ( speed !== undefined )?( speed ):( this.fxSpeed );
- if( this.indicator ){
- this.indicator.hide( speed, callback );
- }
- },
-
// ........................................................................ (error) messages
/** Display a message in the top of the panel.
* @param {String} type type of message ('done', 'error', 'warning')
@@ -964,29 +516,18 @@ var ReadOnlyHistoryPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend
_messageToModalOptions : function( type, msg, details ){
// only error is fleshed out here
var panel = this,
- $modalBody = $( '' ),
options = { title: 'Details' };
-
- //TODO: to some util library
- function objToTable( obj ){
- obj = _.omit( obj, _.functions( obj ) );
- return [
- '
',
- _.map( obj, function( val, key ){
- val = ( _.isObject( val ) )?( objToTable( val ) ):( val );
- return '
' + key + '
'
- + '
' + val + '
';
- }).join( '' ),
- '
'
- ].join( '' );
- }
-
if( _.isObject( details ) ){
- options.body = $modalBody.append( objToTable( details ) );
+
+ details = _.omit( details, _.functions( details ) );
+ var text = JSON.stringify( details, null, ' ' ),
+ pre = $( '' ).text( text );
+ options.body = $( '' ).append( pre );
} else {
- options.body = $modalBody.html( details );
+ options.body = $( '' ).html( details );
}
+
options.buttons = {
'Ok': function(){
Galaxy.modal.hide();
@@ -997,140 +538,91 @@ var ReadOnlyHistoryPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend
return options;
},
- /** Remove all messages from the panel.
- */
- clearMessages : function(){
- this.$messages().empty();
+ /** Remove all messages from the panel. */
+ clearMessages : function( ev ){
+ $( ev.currentTarget ).fadeOut( this.fxSpeed, function(){
+ $( this ).remove();
+ });
+ //this.$messages().children().not( '.quota-message' ).remove();
return this;
},
// ........................................................................ scrolling
- /** get the current scroll position of the panel in its parent */
- scrollPosition : function(){
- return this.$container().scrollTop();
- },
-
- /** set the current scroll position of the panel in its parent */
- scrollTo : function( pos ){
- this.$container().scrollTop( pos );
- return this;
- },
-
- /** Scrolls the panel to the top. */
- scrollToTop : function(){
- this.$container().scrollTop( 0 );
- return this;
- },
-
- /** Scrolls the panel to show the HDA with the given id.
- * @param {String} id the id of HDA to scroll into view
- * @returns {HistoryPanel} the panel
- */
- scrollToId : function( id ){
- // do nothing if id not found
- if( ( !id ) || ( !this.hdaViews[ id ] ) ){
- return this;
- }
- var view = this.hdaViews[ id ];
- //this.scrollIntoView( $viewEl.offset().top );
- this.scrollTo( view.el.offsetTop );
- return this;
- },
-
- /** Scrolls the panel to show the HDA with the given hid.
- * @param {Integer} hid the hid of HDA to scroll into view
+ /** Scrolls the panel to show the content sub-view with the given hid.
+ * @param {Integer} hid the hid of item to scroll into view
* @returns {HistoryPanel} the panel
*/
scrollToHid : function( hid ){
- var hda = this.model.contents.getByHid( hid );
- // do nothing if hid not found
- if( !hda ){ return this; }
- return this.scrollToId( hda.id );
+ return this.scrollToItem( _.first( this.viewsWhereModel({ hid: hid }) ) );
},
// ........................................................................ misc
- print : function(){
- var panel = this;
- panel.debug( this );
- _.each( this.hdaViews, function( view, id ){
- panel.debug( '\t ' + id, view );
- });
- },
-
/** Return a string rep of the history */
toString : function(){
- return 'ReadOnlyHistoryPanel(' + (( this.model )?( this.model.get( 'name' )):( '' )) + ')';
+ return 'HistoryPanel(' + (( this.model )?( this.model.get( 'name' )):( '' )) + ')';
}
});
+
+
//------------------------------------------------------------------------------ TEMPLATES
-var _panelTemplate = [
- '
'
+ ], 'history' );
+
+ return _.extend( _.clone( _super.prototype.templates ), {
+ controls : controlsTemplate
+ });
+}());
//==============================================================================
return {
- ReadOnlyHistoryPanel: ReadOnlyHistoryPanel
+ HistoryPanel: HistoryPanel
};
});
diff --git a/static/scripts/mvc/list/list-item.js b/static/scripts/mvc/list/list-item.js
new file mode 100644
index 00000000000..db503be829e
--- /dev/null
+++ b/static/scripts/mvc/list/list-item.js
@@ -0,0 +1,332 @@
+define([
+ 'mvc/base-mvc',
+ 'utils/localization'
+], function( BASE_MVC, _l ){
+
+//==============================================================================
+/** A view which, when first rendered, shows only summary data/attributes, but
+ * can be expanded to show further details (and optionally fetch those
+ * details from the server).
+ */
+var ExpandableView = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend({
+//TODO: Although the reasoning behind them is different, this shares a lot with HiddenUntilActivated above: combine them
+ //PRECONDITION: model must have method hasDetails
+ //PRECONDITION: subclasses must have templates.el and templates.details
+
+ initialize : function( attributes ){
+ /** are the details of this view expanded/shown or not? */
+ this.expanded = attributes.expanded || false;
+ //this.log( '\t expanded:', this.expanded );
+ this.fxSpeed = attributes.fxSpeed || this.fxSpeed;
+ },
+
+ // ........................................................................ render main
+ /** jq fx speed */
+ fxSpeed : 'fast',
+
+ /** Render this content, set up ui.
+ * @param {Number or String} speed the speed of the render
+ */
+ render : function( speed ){
+ var $newRender = this._buildNewRender();
+ this._setUpBehaviors( $newRender );
+ this._queueNewRender( $newRender, speed );
+ return this;
+ },
+
+ /** Build a temp div containing the new children for the view's $el.
+ * If the view is already expanded, build the details as well.
+ */
+ _buildNewRender : function(){
+ // create a new render using a skeleton template, render title buttons, render body, and set up events, etc.
+ var $newRender = $( this.templates.el( this.model.toJSON(), this ) );
+ if( this.expanded ){
+ this.$details( $newRender ).replaceWith( this._renderDetails().show() );
+ }
+ return $newRender;
+ },
+
+ /** Fade out the old el, swap in the new contents, then fade in.
+ * @param {Number or String} speed jq speed to use for rendering effects
+ * @fires rendered when rendered
+ */
+ _queueNewRender : function( $newRender, speed ) {
+ speed = ( speed === undefined )?( this.fxSpeed ):( speed );
+ var view = this;
+
+ $( view ).queue( 'fx', [
+ function( next ){ this.$el.fadeOut( speed, next ); },
+ function( next ){
+ view._swapNewRender( $newRender );
+ next();
+ },
+ function( next ){ this.$el.fadeIn( speed, next ); },
+ function( next ){
+ this.trigger( 'rendered', view );
+ next();
+ }
+ ]);
+ },
+
+ /** empty out the current el, move the $newRender's children in */
+ _swapNewRender : function( $newRender ){
+ return this.$el.empty().attr( 'class', this.className ).append( $newRender.children() );
+ },
+
+ /** set up js behaviors, event handlers for elements within the given container
+ * @param {jQuery} $container jq object that contains the elements to process (defaults to this.$el)
+ */
+ _setUpBehaviors : function( $where ){
+ $where = $where || this.$el;
+ // set up canned behavior on children (bootstrap, popupmenus, editable_text, etc.)
+ //make_popup_menus( $where );
+ $where.find( '[title]' ).tooltip({ placement : 'bottom' });
+ },
+
+ // ......................................................................... details
+ /** shortcut to details DOM (as jQ) */
+ $details : function( $where ){
+ $where = $where || this.$el;
+ return $where.find( '.details' );
+ },
+
+ /** build the DOM for the details and set up behaviors on it */
+ _renderDetails : function(){
+ var $newDetails = $( this.templates.details( this.model.toJSON(), this ) );
+ this._setUpBehaviors( $newDetails );
+ return $newDetails;
+ },
+
+ // ......................................................................... expansion/details
+ /** Show or hide the details
+ * @param {Boolean} expand if true, expand; if false, collapse
+ */
+ toggleExpanded : function( expand ){
+ expand = ( expand === undefined )?( !this.expanded ):( expand );
+ if( expand ){
+ this.expand();
+ } else {
+ this.collapse();
+ }
+ return this;
+ },
+
+ /** Render and show the full, detailed body of this view including extra data and controls.
+ * note: if the model does not have detailed data, fetch that data before showing the body
+ * @fires expanded when a body has been expanded
+ */
+ expand : function(){
+ var view = this;
+ return view._fetchModelDetails()
+ .always(function(){
+ var $newDetails = view._renderDetails();
+ view.$details().replaceWith( $newDetails );
+ // needs to be set after the above or the slide will not show
+ view.expanded = true;
+ $newDetails.slideDown( view.fxSpeed, function(){
+ view.trigger( 'expanded', view );
+ });
+ });
+ },
+
+ /** Check for model details and, if none, fetch them.
+ * @returns {jQuery.promise} the model.fetch.xhr if details are being fetched, an empty promise if not
+ */
+ _fetchModelDetails : function(){
+ if( !this.model.hasDetails() ){
+ return this.model.fetch();
+ }
+ return jQuery.when();
+ },
+
+ /** Hide the body/details of an HDA.
+ * @fires collapsed when a body has been collapsed
+ */
+ collapse : function(){
+ var view = this;
+ view.expanded = false;
+ this.$details().slideUp( view.fxSpeed, function(){
+ view.trigger( 'collapsed', view );
+ });
+ }
+
+});
+
+
+//==============================================================================
+/** A view that is displayed in some larger list/grid/collection.
+ * Inherits from Expandable, Selectable, Draggable.
+ * The DOM contains warnings, a title bar, and a series of primary action controls.
+ * Primary actions are meant to be easily accessible item functions (such as delete)
+ * that are rendered in the title bar.
+ *
+ * Details are rendered when the user clicks the title bar or presses enter/space when
+ * the title bar is in focus.
+ *
+ * Designed as a base class for history panel contents - but usable elsewhere (I hope).
+ */
+var ListItemView = ExpandableView.extend(
+ BASE_MVC.mixin( BASE_MVC.SelectableViewMixin, BASE_MVC.DraggableViewMixin, {
+
+//TODO: that's a little contradictory
+ tagName : 'div',
+ className : 'list-item',
+
+ /** Set up the base class and all mixins */
+ initialize : function( attributes ){
+ ExpandableView.prototype.initialize.call( this, attributes );
+ BASE_MVC.SelectableViewMixin.initialize.call( this, attributes );
+ BASE_MVC.DraggableViewMixin.initialize.call( this, attributes );
+ },
+
+ // ........................................................................ rendering
+ /** In this override, call methods to build warnings, titlebar and primary actions */
+ _buildNewRender : function(){
+ var $newRender = ExpandableView.prototype._buildNewRender.call( this );
+ $newRender.find( '.warnings' ).replaceWith( this._renderWarnings() );
+ $newRender.find( '.title-bar' ).replaceWith( this._renderTitleBar() );
+ $newRender.find( '.primary-actions' ).append( this._renderPrimaryActions() );
+ $newRender.find( '.subtitle' ).replaceWith( this._renderSubtitle() );
+ return $newRender;
+ },
+
+ /** In this override, render the selector controls and set up dragging before the swap */
+ _swapNewRender : function( $newRender ){
+ ExpandableView.prototype._swapNewRender.call( this, $newRender );
+ if( this.selectable ){ this.showSelector( 0 ); }
+ if( this.draggable ){ this.draggableOn(); }
+ return this.$el;
+ },
+
+ /** Render any warnings the item may need to show (e.g. "I'm deleted") */
+ _renderWarnings : function(){
+ var view = this,
+ $warnings = $( '' ),
+ json = view.model.toJSON();
+//TODO:! unordered (map)
+ _.each( view.templates.warnings, function( templateFn ){
+ $warnings.append( $( templateFn( json, view ) ) );
+ });
+ return $warnings;
+ },
+
+ /** Render the title bar (the main/exposed SUMMARY dom element) */
+ _renderTitleBar : function(){
+ return $( this.templates.titleBar( this.model.toJSON(), this ) );
+ },
+
+ /** Return an array of jQ objects containing common/easily-accessible item controls */
+ _renderPrimaryActions : function(){
+ // override this
+ return [];
+ },
+
+ /** Render the title bar (the main/exposed SUMMARY dom element) */
+ _renderSubtitle : function(){
+ return $( this.templates.subtitle( this.model.toJSON(), this ) );
+ },
+
+ // ......................................................................... events
+ /** event map */
+ events : {
+ // expand the body when the title is clicked or when in focus and space or enter is pressed
+ 'click .title-bar' : '_clickTitleBar',
+ 'keydown .title-bar' : '_keyDownTitleBar',
+
+ // dragging - don't work, originalEvent === null
+ //'dragstart .dataset-title-bar' : 'dragStartHandler',
+ //'dragend .dataset-title-bar' : 'dragEndHandler'
+
+ 'click .selector' : 'toggleSelect'
+ },
+
+ /** expand when the title bar is clicked */
+ _clickTitleBar : function( event ){
+ event.stopPropagation();
+ this.toggleExpanded();
+ },
+
+ /** expand when the title bar is in focus and enter or space is pressed */
+ _keyDownTitleBar : function( event ){
+ // bail (with propagation) if keydown and not space or enter
+ var KEYCODE_SPACE = 32, KEYCODE_RETURN = 13;
+ if( event && ( event.type === 'keydown' )
+ &&( event.keyCode === KEYCODE_SPACE || event.keyCode === KEYCODE_RETURN ) ){
+ this.toggleExpanded();
+ event.stopPropagation();
+ return false;
+ }
+ return true;
+ },
+
+ // ......................................................................... misc
+ /** String representation */
+ toString : function(){
+ var modelString = ( this.model )?( this.model + '' ):( '(no model)' );
+ return 'ListItemView(' + modelString + ')';
+ }
+}));
+
+// ............................................................................ TEMPLATES
+/** underscore templates */
+ListItemView.prototype.templates = (function(){
+//TODO: move to require text! plugin
+
+ var elTemplato = BASE_MVC.wrapTemplate([
+ '
',
+ // space for title bar buttons - gen. floated to the right
+ '',
+ '',
+
+ // expandable area for more details
+ '',
+ '
'
+ ]);
+
+ var warnings = {};
+
+ var titleBarTemplate = BASE_MVC.wrapTemplate([
+ // adding a tabindex here allows focusing the title bar and the use of keydown to expand the dataset display
+ '
',
+//TODO: prob. belongs in dataset-list-item
+ '',
+ '
',
+ '<%- element.name %>',
+ '
',
+ '',
+ '
'
+ ], 'element' );
+
+ var subtitleTemplate = BASE_MVC.wrapTemplate([
+ // override this
+ ''
+ ]);
+
+ var detailsTemplate = BASE_MVC.wrapTemplate([
+ // override this
+ ''
+ ]);
+
+ return {
+ el : elTemplato,
+ warnings : warnings,
+ titleBar : titleBarTemplate,
+ subtitle : subtitleTemplate,
+ details : detailsTemplate
+ };
+}());
+
+
+//==============================================================================
+ return {
+ ExpandableView : ExpandableView,
+ ListItemView : ListItemView
+ };
+});
diff --git a/static/scripts/mvc/dataset/list-panel.js b/static/scripts/mvc/list/list-panel.js
similarity index 58%
rename from static/scripts/mvc/dataset/list-panel.js
rename to static/scripts/mvc/list/list-panel.js
index d645f15c2a5..a1855ea4f30 100644
--- a/static/scripts/mvc/dataset/list-panel.js
+++ b/static/scripts/mvc/list/list-panel.js
@@ -1,12 +1,24 @@
define([
+ "mvc/list/list-item",
"mvc/base-mvc",
"utils/localization"
-], function( BASE_MVC, _l ){
-/* =============================================================================
+], function( LIST_ITEM, BASE_MVC, _l ){
+/* ============================================================================
TODO:
-============================================================================= */
-/** @class List that contains ListItemViews.
+============================================================================ */
+/** @class View for a list/collection of models and the sub-views of those models.
+ * Sub-views must (at least have the interface if not) inherit from ListItemView.
+ * (For a list panel that also includes some 'container' model (History->HistoryContents)
+ * use ModelWithListPanel)
+ *
+ * Allows for:
+ * searching collection/sub-views
+ * selecting/multi-selecting sub-views
+ *
+ * Currently used:
+ * for dataset/dataset-choice
+ * as superclass of ModelListPanel
*/
var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
/** @lends ReadOnlyHistoryPanel.prototype */{
@@ -15,7 +27,9 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
//logger : console,
/** class to use for constructing the sub-views */
- viewClass : BASE_MVC.ListItemView,
+ viewClass : LIST_ITEM.ListItemView,
+ /** class to used for constructing collection of sub-view models */
+ collectionClass : Backbone.Collection,
tagName : 'div',
className : 'list-panel',
@@ -23,10 +37,15 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
/** (in ms) that jquery effects will use */
fxSpeed : 'fast',
- /** string to display when the model has no hdas */
+ /** string to display when the collection has no contents */
emptyMsg : _l( 'This list is empty' ),
- /** string to no hdas match the search terms */
+ /** displayed when no items match the search terms */
noneFoundMsg : _l( 'No matching items found' ),
+ /** string used for search placeholder */
+ searchPlaceholder : _l( 'search' ),
+
+ /** actions available for multiselected items */
+ multiselectActions : [],
// ......................................................................... SET UP
/** Set up the view, set up storage, bind listeners to HistoryContents events
@@ -67,21 +86,42 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
/** list item views */
this.views = [];
/** list item models */
- this.collection = attributes.collection || ( new Backbone.Collection([]) );
+ this.collection = attributes.collection || ( new this.collectionClass([]) );
/** filter fns run over collection items to see if they should show in the list */
this.filters = attributes.filters || [];
+ /** override $scrollContainer fn via attributes - fn should return jq for elem to call scrollTo on */
+ this.$scrollContainer = attributes.$scrollContainer || this.$scrollContainer;
+
//TODO: remove
this.title = attributes.title || '';
this.subtitle = attributes.subtitle || '';
+ // allow override of multiselectActions through attributes
+ this.multiselectActions = attributes.multiselectActions || this.multiselectActions;
+ /** the popup displayed when 'for all selected...' is clicked */
+ this.actionsPopup = null;
+
this._setUpListeners();
},
+ /** free any sub-views the list has */
+ freeViews : function(){
+//TODO: stopListening? remove?
+ _.each( this.views, function( view ){
+ view.off();
+ });
+ this.views = [];
+ return this;
+ },
+
+ // ------------------------------------------------------------------------ listeners
/** create any event listeners for the list
*/
_setUpListeners : function(){
+ this.off();
+
this.on( 'error', function( model, xhr, options, msg, details ){
//this.errorHandler( model, xhr, options, msg, details );
console.error( model, xhr, options, msg, details );
@@ -112,21 +152,19 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
return this;
},
- /** free any sub-views the list has */
- freeViews : function(){
-//TODO: stopListening? remove?
- this.views = [];
- return this;
- },
-
- // ------------------------------------------------------------------------ item listeners
- /** listening for history and HDA events */
+ /** listening for collection events */
_setUpCollectionListeners : function(){
+ this.log( this + '._setUpCollectionListeners', this.collection );
+ this.collection.off();
+
+ // bubble up error events
+ this.collection.on( 'error', function( model, xhr, options, msg, details ){
+ this.trigger( 'error', model, xhr, options, msg, details );
+ }, this );
this.collection.on( 'reset', function(){
this.renderItems();
}, this );
-
this.collection.on( 'add', this.addItemView, this );
this.collection.on( 'remove', this.removeItemView, this );
@@ -139,8 +177,9 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
return this;
},
- /** listening for history and HDA events */
+ /** listening for sub-view events that bubble up with the 'view:' prefix */
_setUpViewListeners : function(){
+ this.log( this + '._setUpViewListeners' );
// shift to select a range
this.on( 'view:selected', function( view, ev ){
@@ -155,6 +194,11 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
this.selected.push( view.model.id );
this.lastSelected = view.model.id;
}, this );
+
+ this.on( 'view:de-selected', function( view, ev ){
+ this.selected = _.without( this.selected, view.model.id );
+ //this.lastSelected = view.model.id;
+ }, this );
},
// ------------------------------------------------------------------------ rendering
@@ -162,6 +206,7 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
* @param {Number or String} speed the speed of the render
*/
render : function( speed ){
+ this.log( this + '.render', speed );
var $newRender = this._buildNewRender();
this._setUpBehaviors( $newRender );
this._queueNewRender( $newRender, speed );
@@ -171,9 +216,9 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
/** Build a temp div containing the new children for the view's $el.
*/
_buildNewRender : function(){
- // create a new render using a skeleton template, render title buttons, render body, and set up events, etc.
- var json = this.model? this.model.toJSON() : {},
- $newRender = $( this.templates.el( json, this ) );
+ this.debug( this + '(ListPanel)._buildNewRender' );
+ var $newRender = $( this.templates.el( {}, this ) );
+ this._renderControls( $newRender );
this._renderTitle( $newRender );
this._renderSubtitle( $newRender );
this._renderSearch( $newRender );
@@ -181,6 +226,15 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
return $newRender;
},
+ /** Build a temp div containing the new children for the view's $el.
+ */
+ _renderControls : function( $newRender ){
+ this.debug( this + '(ListPanel)._renderControls' );
+ var $controls = $( this.templates.controls( {}, this ) );
+ $newRender.find( '.controls' ).replaceWith( $controls );
+ return $controls;
+ },
+
/**
*/
_renderTitle : function( $where ){
@@ -201,17 +255,18 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
*/
_queueNewRender : function( $newRender, speed ) {
speed = ( speed === undefined )?( this.fxSpeed ):( speed );
- var view = this;
+ var panel = this;
+ panel.log( '_queueNewRender:', $newRender, speed );
- $( view ).queue( 'fx', [
+ $( panel ).queue( 'fx', [
function( next ){ this.$el.fadeOut( speed, next ); },
function( next ){
- view._swapNewRender( $newRender );
+ panel._swapNewRender( $newRender );
next();
},
function( next ){ this.$el.fadeIn( speed, next ); },
function( next ){
- view.trigger( 'rendered', view );
+ panel.trigger( 'rendered', panel );
next();
}
]);
@@ -234,7 +289,7 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
// ------------------------------------------------------------------------ sub-$element shortcuts
/** the scroll container for this panel - can be $el, $el.parent(), or grandparent depending on context */
$scrollContainer : function(){
- // override
+ // override or set via attributes.$scrollContainer
return this.$el.parent().parent();
},
/** */
@@ -242,8 +297,8 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
return ( $where || this.$el ).find( '.list-items' );
},
/** container where list messages are attached */
- $messages : function( $where ){
- return ( $where || this.$el ).find( '.message-container' );
+ $messages : function( $where ){
+ return ( $where || this.$el ).find( '.messages' );
},
/** the message displayed when no views can be shown (no views, none matching search) */
$emptyMessage : function( $where ){
@@ -252,52 +307,50 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
// ------------------------------------------------------------------------ hda sub-views
/**
- * @param {jQuery} $whereTo what dom element to prepend the HDA views to
+ * @param {jQuery} $whereTo what dom element to prepend the sub-views to
* @returns the visible item views
*/
renderItems : function( $whereTo ){
$whereTo = $whereTo || this.$el;
- var list = this,
- newViews = [];
+ var panel = this;
+ panel.log( this + '.renderItems', $whereTo );
- var $list = this.$list( $whereTo ),
- item$els = this._filterCollection().map( function( itemModel ){
+ var $list = panel.$list( $whereTo );
+//TODO: free prev. views?
+ panel.views = panel._filterCollection().map( function( itemModel ){
//TODO: creates views each time - not neccessarily good
- var view = list._createItemView( itemModel );
- newViews.push( view );
- return view.render( 0 ).$el;
+ return panel._createItemView( itemModel ).render( 0 );
});
- this.debug( item$els );
- this.debug( newViews );
+ //panel.debug( item$els );
+ //panel.debug( newViews );
$list.empty();
- if( item$els.length ){
- $list.append( item$els );
- this.$emptyMessage( $whereTo ).hide();
+ if( panel.views.length ){
+ panel._attachItems( $whereTo );
+ panel.$emptyMessage( $whereTo ).hide();
} else {
- this._renderEmptyMessage( $whereTo ).show();
+ panel._renderEmptyMessage( $whereTo ).show();
}
- this.views = newViews;
- return newViews;
+ return panel.views;
},
/**
*/
_filterCollection : function(){
// override this
- var list = this;
- return list.collection.filter( _.bind( list._filterItem, list ) );
+ var panel = this;
+ return panel.collection.filter( _.bind( panel._filterItem, panel ) );
},
/**
*/
_filterItem : function( model ){
// override this
- var list = this;
- return ( _.every( list.filters.map( function( fn ){ return fn.call( model ); }) ) )
- && ( !list.searchFor || model.matchesAll( list.searchFor ) );
+ var panel = this;
+ return ( _.every( panel.filters.map( function( fn ){ return fn.call( model ); }) ) )
+ && ( !panel.searchFor || model.matchesAll( panel.searchFor ) );
},
/**
@@ -312,11 +365,15 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
return view;
},
+ /**
+ */
_getItemViewClass : function( model ){
// override this
return this.viewClass;
},
+ /**
+ */
_getItemViewOptions : function( model ){
// override this
return {
@@ -332,11 +389,12 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
/**
*/
_setUpItemViewListeners : function( view ){
- var list = this;
+ var panel = this;
+ // send all events to the panel, re-namspaceing them with the view prefix
view.on( 'all', function(){
var args = Array.prototype.slice.call( arguments, 0 );
args[0] = 'view:' + args[0];
- list.trigger.apply( list, args );
+ panel.trigger.apply( panel, args );
});
// debugging
@@ -345,12 +403,21 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
// this.log( this + '(view)', arguments );
// }, this );
//}
+ return panel;
+ },
+
+ /**
+ */
+ _attachItems : function( $whereTo ){
+ this.$list( $whereTo ).append( this.views.map( function( view ){
+ return view.$el;
+ }));
return this;
},
/** render the empty/none-found message */
_renderEmptyMessage : function( $whereTo ){
- //this.debug( '_renderEmptyMessage', $whereTo, this.searchFor );
+ this.debug( '_renderEmptyMessage', $whereTo, this.searchFor );
var text = this.searchFor? this.noneFoundMsg : this.emptyMsg;
return this.$emptyMessage( $whereTo ).text( text );
},
@@ -374,37 +441,45 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
*/
addItemView : function( model, collection, options ){
this.log( this + '.addItemView:', model );
- var list = this;
- if( !this._filterItem( model ) ){ return undefined; }
+ var panel = this;
+ if( !panel._filterItem( model ) ){ return undefined; }
//TODO: sorted? position?
- var view = list._createItemView( model );
- this.views.push( view );
+ var view = panel._createItemView( model );
+ panel.views.push( view );
$( view ).queue( 'fx', [
- function( next ){ list.$emptyMessage().fadeOut( list.fxSpeed, next ); },
+ function( next ){ panel.$emptyMessage().fadeOut( panel.fxSpeed, next ); },
function( next ){
//TODO: auto render?
- list.$list().append( view.render().$el );
+// slide down?
+ panel.$list().append( view.render().$el );
next();
}
]);
return view;
},
- /**
- */
+ /** */
removeItemView : function( model, collection, options ){
this.log( this + '.removeItemView:', model );
- var list = this,
- view = list.viewFromModel( model );
+ var panel = this,
+ view = panel.viewFromModel( model );
if( !view ){ return undefined; }
- this.views = _.without( this.views, view );
- view.remove();
- if( !this.views.length ){
- list._renderEmptyMessage().fadeIn( list.fxSpeed );
- }
+ // use anonymous queue here - since remove can happen multiple times
+ $({}).queue( 'fx', [
+ function( next ){ view.$el.fadeOut( panel.fxSpeed, next ); },
+ function( next ){
+ panel.views = _.without( panel.views, view );
+ view.remove();
+ if( !panel.views.length ){
+ panel._renderEmptyMessage().fadeIn( panel.fxSpeed, next );
+ } else {
+ next();
+ }
+ }
+ ]);
return view;
},
@@ -420,6 +495,16 @@ var ListPanel = Backbone.View.extend( BASE_MVC.LoggableMixin ).extend(
return undefined;
},
+ /** get views based on model.id */
+ viewFromModelId : function( id ){
+ for( var i=0; i',
- '
',
- '
',
- '
<%= model.name || view.title %>
',
- '
',
- '
<%= view.subtitle %>
',
- '',
- '',
-
- '
',
- '',
- '
',
-
- '
',
- '
',
- '',
- '',
- '
',
- //'',
- '
',
- '
',
+ '',
'',
'',
'
'
]);
+ var controlsTemplate = BASE_MVC.wrapTemplate([
+ '
',
+ '
',
+ '
<%= view.title %>
',
+ '
',
+ '
<%= view.subtitle %>
',
+ '',
+ '',
+
+ '
',
+ '',
+ '
',
+
+ '
',
+ '
',
+ '',
+ '',
+ '
',
+ //'',
+ '
',
+ '
'
+ ]);
+
return {
- el : elTemplate
+ el : elTemplate,
+ controls : controlsTemplate
};
}());
+//=============================================================================
+/** View for a model that has a sub-collection (e.g. History, DatasetCollection)
+ *
+ *
+ */
+var ModelListPanel = ListPanel.extend({
-//==============================================================================
+ /** key of attribute in model to assign to this.collection */
+ modelCollectionKey : 'contents',
+
+ initialize : function( attributes ){
+ ListPanel.prototype.initialize.call( this, attributes );
+ this.selecting = ( attributes.selecting !== undefined )? attributes.selecting : false;
+
+ // ---- instance vars
+ // don't render when setting the first time
+ this.setModel( this.model, attributes );
+ },
+
+ /** release/free/shutdown old models and set up panel for new models
+ * @fires new-model with the panel as parameter
+ */
+ setModel : function( model, attributes ){
+ attributes = attributes || {};
+ this.debug( this + '.setModel:', model, attributes );
+
+ this.freeModel();
+ this.freeViews();
+
+ if( model ){
+ // set up the new model with user, logger, storage, events
+ this.model = model;
+ if( this.logger ){
+ this.model.logger = this.logger;
+ }
+ this._setUpModelListeners();
+
+//TODO: relation btwn model, collection becoming tangled here
+ // free the collection, and assign the new collection to either
+ // the model[ modelCollectionKey ], attributes.collection, or an empty vanilla collection
+ this.collection.off();
+ this.collection = ( this.model[ this.modelCollectionKey ] )?
+ this.model[ this.modelCollectionKey ]:
+ ( attributes.collection || ( new this.collectionClass([]) ) );
+ this._setUpCollectionListeners();
+
+ this.trigger( 'new-model', this );
+ }
+ return this;
+ },
+
+ /** free the current model and all listeners for it, free any views for the model */
+ freeModel : function(){
+ // stop/release the previous model, and clear cache to sub-views
+ if( this.model ){
+ this.stopListening( this.model );
+ //TODO: see base-mvc
+ //this.model.free();
+ //this.model = null;
+ }
+ return this;
+ },
+
+ // ------------------------------------------------------------------------ listening
+ /** listening for model events */
+ _setUpModelListeners : function(){
+ // override
+ this.log( this + '._setUpModelListeners', this.model );
+ // bounce model errors up to the panel
+ this.model.on( 'error', function(){
+ //TODO: namespace?
+ //var args = Array.prototype.slice.call( arguments, 0 );
+ //args[0] = 'model:' + args[0];
+ this.trigger.apply( panel, arguments );
+ }, this );
+ return this;
+ },
+
+ /** Build a temp div containing the new children for the view's $el.
+ */
+ _renderControls : function( $newRender ){
+ this.debug( this + '(ListPanel)._renderControls' );
+ var json = this.model? this.model.toJSON() : {},
+ $controls = $( this.templates.controls( json, this ) );
+ $newRender.find( '.controls' ).replaceWith( $controls );
+ this.debug( '\t .controls:', this.$( '.controls' ) );
+ this.debug( '\t $controls:', $controls );
+ return $controls;
+ },
+
+ // ------------------------------------------------------------------------ misc
+ /** Return a string rep of the panel */
+ toString : function(){
+ return 'ModelListPanel(' + this.model + ')';
+ }
+});
+
+// ............................................................................ TEMPLATES
+/** underscore templates */
+ModelListPanel.prototype.templates = (function(){
+//TODO: move to require text! plugin
+
+ var controlsTemplate = BASE_MVC.wrapTemplate([
+ '
',
+ '
',
+ '
<%= model.name %>
',
+ '
',
+ '
<%= view.subtitle %>
',
+ '',
+ '',
+
+ '
',
+ '',
+ '
',
+
+ '
',
+ '
',
+ '',
+ '',
+ '
',
+ //'',
+ '
',
+ '
'
+ ]);
+
+ return _.extend( _.clone( ListPanel.prototype.templates ), {
+ controls : controlsTemplate
+ });
+}());
+
+
+//=============================================================================
return {
- ListPanel: ListPanel
+ ListPanel : ListPanel,
+ ModelListPanel : ModelListPanel
};
});
diff --git a/static/scripts/mvc/tools/tools-form.js b/static/scripts/mvc/tools/tools-form.js
index 4b017209afe..bcd79081411 100644
--- a/static/scripts/mvc/tools/tools-form.js
+++ b/static/scripts/mvc/tools/tools-form.js
@@ -157,6 +157,7 @@ define(['mvc/ui/ui-portlet', 'mvc/ui/ui-misc',
// trigger refresh
self.refresh();
+ self._submit();
}
});
},
@@ -177,7 +178,7 @@ define(['mvc/ui/ui-portlet', 'mvc/ui/ui-misc',
// submit
_submit: function() {
- console.log(this.tree.jobDictionary());
+ console.log(this.tree.finalize());
}
});
diff --git a/static/scripts/mvc/tools/tools-tree.js b/static/scripts/mvc/tools/tools-tree.js
index db50641434d..c27d23e0442 100644
--- a/static/scripts/mvc/tools/tools-tree.js
+++ b/static/scripts/mvc/tools/tools-tree.js
@@ -11,24 +11,23 @@ return Backbone.Model.extend({
// creates tree structure
refresh: function() {
- // check if section is available
- if (!this.app.section) {
- return {};
- }
-
// create dictionary
this.dict = {};
// create xml object
this.xml = $('');
+ // check if section is available
+ if (!this.app.section) {
+ return {};
+ }
+
// fill dictionary
this._iterate(this.app.section.$el, this.dict, this.xml);
},
-
// convert to job dictionary
- jobDictionary: function() {
+ finalize: function() {
// link this
var self = this;
@@ -38,22 +37,48 @@ return Backbone.Model.extend({
// converter between raw dictionary and job dictionary
function convert(identifier, head) {
for (var i in head) {
- if (head[i].input) {
- var input = head[i].input;
- var job_input_id = identifier + input.name;
+ var node = head[i];
+ if (node.input) {
+ // get node
+ var input = node.input;
- if (input.type == 'repeat') {
-
+ // create identifier
+ var job_input_id = identifier;
+ if (identifier != '') {
+ job_input_id += '|';
+ }
+ job_input_id += input.name;
- } else {
- //var value = self.app.field_list[input.id].value();
- switch(input.type) {
- case 'repeat':
- job_def[job_input_id] = value;
- break;
- default:
- job_def[job_input_id] = value;
- }
+ // process input type
+ switch (input.type) {
+ // handle repeats
+ case 'repeat':
+ var index = 0;
+ for (var j in node) {
+ if (j.indexOf('section') != -1) {
+ convert(job_input_id + '_' + index++, node[j]);
+ }
+ }
+ break;
+ // handle conditionals
+ case 'conditional':
+ // get conditional value
+ var value = self.app.field_list[input.id].value();
+
+ // find selected case
+ for (var j in input.cases) {
+ if (input.cases[j].value == value) {
+ convert(job_input_id, head[input.id + '-section-' + j]);
+ break;
+ }
+ }
+
+ // break
+ break;
+ default:
+ // handle default value
+ var value = self.app.field_list[input.id].value();
+ job_def[job_input_id] = value;
}
}
}
@@ -61,49 +86,10 @@ return Backbone.Model.extend({
// start conversion
convert('', this.dict);
-
+
// return result
return job_def;
},
-
- // iterate
- _iterate: function(parent, dict, xml) {
- // get child nodes
- var self = this;
- var children = $(parent).children();
- children.each(function() {
- // get child element
- var child = this;
-
- // get id
- var id = $(child).attr('id');
-
- // create new branch
- if ($(child).hasClass('section-row') || $(child).hasClass('tab-pane')) {
- // create sub dictionary
- dict[id] = {};
-
- // add input element if it exists
- var input = self.app.input_list[id];
- if (input) {
- dict[id] = {
- input : input
- }
- }
-
- // create xml element
- var $el = $('');
-
- // append xml
- xml.append($el);
-
- // fill sub dictionary
- self._iterate(child, dict[id], $el);
- } else {
- self._iterate(child, dict, xml);
- }
- });
- },
// find referenced elements
findReferences: function(identifier, type) {
@@ -176,6 +162,45 @@ return Backbone.Model.extend({
// return
return referenced;
+ },
+
+ // iterate
+ _iterate: function(parent, dict, xml) {
+ // get child nodes
+ var self = this;
+ var children = $(parent).children();
+ children.each(function() {
+ // get child element
+ var child = this;
+
+ // get id
+ var id = $(child).attr('id');
+
+ // create new branch
+ if ($(child).hasClass('section-row') || $(child).hasClass('tab-pane')) {
+ // create sub dictionary
+ dict[id] = {};
+
+ // add input element if it exists
+ var input = self.app.input_list[id];
+ if (input) {
+ dict[id] = {
+ input : input
+ }
+ }
+
+ // create xml element
+ var $el = $('');
+
+ // append xml
+ xml.append($el);
+
+ // fill sub dictionary
+ self._iterate(child, dict[id], $el);
+ } else {
+ self._iterate(child, dict, xml);
+ }
+ });
}
});
diff --git a/static/scripts/mvc/ui.js b/static/scripts/mvc/ui.js
index 3bd8859b15b..709923a2d64 100644
--- a/static/scripts/mvc/ui.js
+++ b/static/scripts/mvc/ui.js
@@ -612,7 +612,7 @@ function LoadingIndicator( $where, options ){
* of characters, a callback is called. Pressing ESC when the input
* is focused will clear the input and call a separate callback.
*/
- var _l = window._l || function( s ){ return s; }
+ var _l = window._l || function( s ){ return s; };
// contructor
function searchInput( parentNode, options ){
@@ -637,11 +637,9 @@ function LoadingIndicator( $where, options ){
// .................................................................... input rendering and events
// visually clear the search, trigger an event, and call the callback
function clearSearchInput( event ){
- //console.debug( this, 'clear' );
var $input = $( this ).parent().children( 'input' );
- //console.debug( 'input', $input );
- $input.val( '' );
- $input.trigger( 'clear:searchInput' );
+ //console.debug( this, 'clear', $input );
+ $input.focus().val( '' ).trigger( 'clear:searchInput' );
options.onclear();
}
diff --git a/static/scripts/mvc/ui/ui-tabs.js b/static/scripts/mvc/ui/ui-tabs.js
index 21d08563673..5b990791ee8 100644
--- a/static/scripts/mvc/ui/ui-tabs.js
+++ b/static/scripts/mvc/ui/ui-tabs.js
@@ -20,7 +20,8 @@ var View = Backbone.View.extend({
this.$nav = null;
this.$content = null;
this.first_tab = null;
-
+ this.current_id = null;
+
// configure options
this.options = Utils.merge(options, this.optionsDefault);
@@ -139,6 +140,11 @@ var View = Backbone.View.extend({
self.show(id);
}
});
+
+ // initialize current id
+ if (!this.current_id) {
+ this.current_id = id;
+ }
},
// delete tab
@@ -183,10 +189,14 @@ var View = Backbone.View.extend({
// show selected tab
if (id) {
- this.$el.find('.tab-element').removeClass('active');
- this.$el.find('.tab-pane').removeClass('active');
+ // reassign active class
+ this.$el.find('#tab-' + this.current_id).removeClass('active');
+ this.$el.find('#' + this.current_id).removeClass('active');
this.$el.find('#tab-' + id).addClass('active');
this.$el.find('#' + id).addClass('active');
+
+ // update current id
+ this.current_id = id;
}
// change
diff --git a/static/scripts/packed/mvc/base-mvc.js b/static/scripts/packed/mvc/base-mvc.js
index 9e7d7ef7f64..3d4fc20f8b7 100644
--- a/static/scripts/packed/mvc/base-mvc.js
+++ b/static/scripts/packed/mvc/base-mvc.js
@@ -1 +1 @@
-define(["utils/add-logging","utils/localization"],function(j,b){var k={logger:null,_logNamespace:"?",log:function(){if(this.logger){var m=this.logger.log;if(typeof this.logger.log==="object"){m=Function.prototype.bind.call(this.logger.log,this.logger)}return m.apply(this.logger,arguments)}return undefined}};j(k);var a=Backbone.Model.extend({initialize:function(n){this._checkEnabledSessionStorage();if(!n.id){throw new Error("SessionStorageModel requires an id in the initial attributes")}this.id=n.id;var m=(!this.isNew())?(this._read(this)):({});this.clear({silent:true});this.save(_.extend({},this.defaults,m,n),{silent:true});this.on("change",function(){this.save()})},_checkEnabledSessionStorage:function(){try{return sessionStorage.length}catch(m){alert("Please enable cookies in your browser for this Galaxy site");return false}},sync:function(p,n,m){if(!m.silent){n.trigger("request",n,{},m)}var o;switch(p){case"create":o=this._create(n);break;case"read":o=this._read(n);break;case"update":o=this._update(n);break;case"delete":o=this._delete(n);break}if(o!==undefined||o!==null){if(m.success){m.success()}}else{if(m.error){m.error()}}return o},_create:function(m){var n=m.toJSON(),o=sessionStorage.setItem(m.id,JSON.stringify(n));return(o===null)?(o):(n)},_read:function(m){return JSON.parse(sessionStorage.getItem(m.id))},_update:function(m){return m._create(m)},_delete:function(m){return sessionStorage.removeItem(m.id)},isNew:function(){return !sessionStorage.hasOwnProperty(this.id)},_log:function(){return JSON.stringify(this.toJSON(),null," ")},toString:function(){return"SessionStorageModel("+this.id+")"}});(function(){a.prototype=_.omit(a.prototype,"url","urlRoot")}());var h={searchAttributes:[],searchAliases:{},searchAttribute:function(o,m){var n=this.get(o);if(!m||(n===undefined||n===null)){return false}if(_.isArray(n)){return this._searchArrayAttribute(n,m)}return(n.toString().toLowerCase().indexOf(m.toLowerCase())!==-1)},_searchArrayAttribute:function(n,m){m=m.toLowerCase();return _.any(n,function(o){return(o.toString().toLowerCase().indexOf(m.toLowerCase())!==-1)})},search:function(m){var n=this;return _.filter(this.searchAttributes,function(o){return n.searchAttribute(o,m)})},matches:function(n){var p="=",m=n.split(p);if(m.length>=2){var o=m[0];o=this.searchAliases[o]||o;return this.searchAttribute(o,m[1])}return !!this.search(n).length},matchesAll:function(n){var m=this;n=n.match(/(".*"|\w*=".*"|\S*)/g).filter(function(o){return !!o});return _.all(n,function(o){o=o.replace(/"/g,"");return m.matches(o)})}};var d={hiddenUntilActivated:function(m,o){o=o||{};this.HUAVOptions={$elementShown:this.$el,showFn:jQuery.prototype.toggle,showSpeed:"fast"};_.extend(this.HUAVOptions,o||{});this.HUAVOptions.hasBeenShown=this.HUAVOptions.$elementShown.is(":visible");this.hidden=this.isHidden();if(m){var n=this;m.on("click",function(p){n.toggle(n.HUAVOptions.showSpeed)})}},isHidden:function(){return(this.HUAVOptions.$elementShown.is(":hidden"))},toggle:function(){if(this.hidden){if(!this.HUAVOptions.hasBeenShown){if(_.isFunction(this.HUAVOptions.onshowFirstTime)){this.HUAVOptions.hasBeenShown=true;this.HUAVOptions.onshowFirstTime.call(this)}}if(_.isFunction(this.HUAVOptions.onshow)){this.HUAVOptions.onshow.call(this);this.trigger("hiddenUntilActivated:shown",this)}this.hidden=false}else{if(_.isFunction(this.HUAVOptions.onhide)){this.HUAVOptions.onhide.call(this);this.trigger("hiddenUntilActivated:hidden",this)}this.hidden=true}return this.HUAVOptions.showFn.apply(this.HUAVOptions.$elementShown,arguments)}};function l(p,o){var m=Array.prototype.slice.call(arguments,0),n=m.pop();m.unshift(n);return _.defaults.apply(_,m)}function f(n,m){m=m||"model";var o=_.template(n.join(""));return function(q,p){var r={view:p||{},_l:b};r[m]=q||{};return o(r)}}var c=Backbone.View.extend(k).extend({initialize:function(m){this.expanded=m.expanded||false;this.fxSpeed=m.fxSpeed||this.fxSpeed},fxSpeed:"fast",render:function(n){var m=this._buildNewRender();this._setUpBehaviors(m);this._queueNewRender(m,n);return this},_buildNewRender:function(){var m=$(this.templates.el(this.model.toJSON(),this));if(this.expanded){this.$details(m).replaceWith(this._renderDetails().show())}return m},_queueNewRender:function(n,o){o=(o===undefined)?(this.fxSpeed):(o);var m=this;$(m).queue("fx",[function(p){this.$el.fadeOut(o,p)},function(p){m._swapNewRender(n);p()},function(p){this.$el.fadeIn(o,p)},function(p){this.trigger("rendered",m);p()}])},_swapNewRender:function(m){return this.$el.empty().attr("class",this.className).append(m.children())},_setUpBehaviors:function(m){m=m||this.$el;m.find("[title]").tooltip({placement:"bottom"})},$details:function(m){m=m||this.$el;return m.find(".details")},_renderDetails:function(){var m=$(this.templates.details(this.model.toJSON(),this));this._setUpBehaviors(m);return m},toggleExpanded:function(m){m=(m===undefined)?(!this.expanded):(m);if(m){this.expand()}else{this.collapse()}return this},expand:function(){var m=this;return m._fetchModelDetails().always(function(){var n=m._renderDetails();m.$details().replaceWith(n);m.expanded=true;n.slideDown(m.fxSpeed,function(){m.trigger("expanded",m)})})},_fetchModelDetails:function(){if(!this.model.hasDetails()){return this.model.fetch()}return jQuery.when()},collapse:function(){var m=this;m.expanded=false;this.$details().slideUp(m.fxSpeed,function(){m.trigger("collapsed",m)})}});var g={initialize:function(m){this.draggable=m.draggable||false},$dragHandle:function(){return this.$(".title-bar")},toggleDraggable:function(){if(this.draggable){this.draggableOff()}else{this.draggableOn()}},draggableOn:function(){this.draggable=true;this.dragStartHandler=_.bind(this._dragStartHandler,this);this.dragEndHandler=_.bind(this._dragEndHandler,this);var m=this.$dragHandle().attr("draggable",true).get(0);m.addEventListener("dragstart",this.dragStartHandler,false);m.addEventListener("dragend",this.dragEndHandler,false)},draggableOff:function(){this.draggable=false;var m=this.$dragHandle().attr("draggable",false).get(0);m.removeEventListener("dragstart",this.dragStartHandler,false);m.removeEventListener("dragend",this.dragEndHandler,false)},_dragStartHandler:function(m){this.trigger("dragstart",this);m.dataTransfer.effectAllowed="move";m.dataTransfer.setData("text",JSON.stringify(this.model.toJSON()));return false},_dragEndHandler:function(m){this.trigger("dragend",this);return false}};var e={initialize:function(m){this.selectable=m.selectable||false;this.selected=m.selected||false},$selector:function(){return this.$(".selector")},_renderSelected:function(){this.$selector().find("span").toggleClass("fa-check-square-o",this.selected).toggleClass("fa-square-o",!this.selected)},toggleSelector:function(){if(!this.$selector().is(":visible")){this.showSelector()}else{this.hideSelector()}},showSelector:function(m){m=m!==undefined?m:this.fxSpeed;this.selectable=true;this.trigger("selectable",true,this);this._renderSelected();this.$selector().show(m)},hideSelector:function(m){m=m!==undefined?m:this.fxSpeed;this.selectable=false;this.trigger("selectable",false,this);this.$selector().hide(m)},toggleSelect:function(m){if(this.selected){this.deselect(m)}else{this.select(m)}},select:function(m){if(!this.selected){this.trigger("selected",this,m);this.selected=true;this._renderSelected()}return false},deselect:function(m){if(this.selected){this.trigger("de-selected",this,m);this.selected=false;this._renderSelected()}return false}};var i=c.extend(l(e,g,{tagName:"div",className:"list-item",initialize:function(m){c.prototype.initialize.call(this,m);e.initialize.call(this,m);g.initialize.call(this,m)},_buildNewRender:function(){var m=c.prototype._buildNewRender.call(this);m.find(".warnings").replaceWith(this._renderWarnings());m.find(".title-bar").replaceWith(this._renderTitleBar());m.find(".primary-actions").append(this._renderPrimaryActions());m.find(".subtitle").replaceWith(this._renderSubtitle());return m},_swapNewRender:function(m){c.prototype._swapNewRender.call(this,m);if(this.selectable){this.showSelector(0)}if(this.draggable){this.draggableOn()}return this.$el},_renderWarnings:function(){var m=this,o=$(''),n=m.model.toJSON();_.each(m.templates.warnings,function(p){o.append($(p(n,m)))});return o},_renderTitleBar:function(){return $(this.templates.titleBar(this.model.toJSON(),this))},_renderPrimaryActions:function(){return[]},_renderSubtitle:function(){return $(this.templates.subtitle(this.model.toJSON(),this))},events:{"click .title-bar":"_clickTitleBar","keydown .title-bar":"_keyDownTitleBar","click .selector":"toggleSelect"},_clickTitleBar:function(m){m.stopPropagation();this.toggleExpanded()},_keyDownTitleBar:function(o){var m=32,n=13;if(o&&(o.type==="keydown")&&(o.keyCode===m||o.keyCode===n)){this.toggleExpanded();o.stopPropagation();return false}return true},toString:function(){var m=(this.model)?(this.model+""):("(no model)");return"ListItemView("+m+")"}}));i.prototype.templates=(function(){var o=f(['
','','
','',"
",'','','',"
"]);var m={};var p=f(['
','','
','<%- element.name %>',"
",'',"
"],"element");var q=f(['']);var n=f(['']);return{el:o,warnings:m,titleBar:p,subtitle:q,details:n}}());return{LoggableMixin:k,SessionStorageModel:a,SearchableModelMixin:h,HiddenUntilActivatedViewMixin:d,mixin:l,wrapTemplate:f,ExpandableView:c,DraggableViewMixin:g,SelectableViewMixin:e,ListItemView:i}});
\ No newline at end of file
+define(["utils/add-logging","utils/localization"],function(h,b){var i={logger:null,_logNamespace:"?",log:function(){if(this.logger){var k=this.logger.log;if(typeof this.logger.log==="object"){k=Function.prototype.bind.call(this.logger.log,this.logger)}return k.apply(this.logger,arguments)}return undefined}};h(i);var a=Backbone.Model.extend({initialize:function(l){this._checkEnabledSessionStorage();if(!l.id){throw new Error("SessionStorageModel requires an id in the initial attributes")}this.id=l.id;var k=(!this.isNew())?(this._read(this)):({});this.clear({silent:true});this.save(_.extend({},this.defaults,k,l),{silent:true});this.on("change",function(){this.save()})},_checkEnabledSessionStorage:function(){try{return sessionStorage.length}catch(k){alert("Please enable cookies in your browser for this Galaxy site");return false}},sync:function(n,l,k){if(!k.silent){l.trigger("request",l,{},k)}var m;switch(n){case"create":m=this._create(l);break;case"read":m=this._read(l);break;case"update":m=this._update(l);break;case"delete":m=this._delete(l);break}if(m!==undefined||m!==null){if(k.success){k.success()}}else{if(k.error){k.error()}}return m},_create:function(k){var l=k.toJSON(),m=sessionStorage.setItem(k.id,JSON.stringify(l));return(m===null)?(m):(l)},_read:function(k){return JSON.parse(sessionStorage.getItem(k.id))},_update:function(k){return k._create(k)},_delete:function(k){return sessionStorage.removeItem(k.id)},isNew:function(){return !sessionStorage.hasOwnProperty(this.id)},_log:function(){return JSON.stringify(this.toJSON(),null," ")},toString:function(){return"SessionStorageModel("+this.id+")"}});(function(){a.prototype=_.omit(a.prototype,"url","urlRoot")}());function j(n,m){var k=Array.prototype.slice.call(arguments,0),l=k.pop();k.unshift(l);return _.defaults.apply(_,k)}var g={searchAttributes:[],searchAliases:{},searchAttribute:function(m,k){var l=this.get(m);if(!k||(l===undefined||l===null)){return false}if(_.isArray(l)){return this._searchArrayAttribute(l,k)}return(l.toString().toLowerCase().indexOf(k.toLowerCase())!==-1)},_searchArrayAttribute:function(l,k){k=k.toLowerCase();return _.any(l,function(m){return(m.toString().toLowerCase().indexOf(k.toLowerCase())!==-1)})},search:function(k){var l=this;return _.filter(this.searchAttributes,function(m){return l.searchAttribute(m,k)})},matches:function(l){var n="=",k=l.split(n);if(k.length>=2){var m=k[0];m=this.searchAliases[m]||m;return this.searchAttribute(m,k[1])}return !!this.search(l).length},matchesAll:function(l){var k=this;l=l.match(/(".*"|\w*=".*"|\S*)/g).filter(function(m){return !!m});return _.all(l,function(m){m=m.replace(/"/g,"");return k.matches(m)})}};var c={hiddenUntilActivated:function(k,m){m=m||{};this.HUAVOptions={$elementShown:this.$el,showFn:jQuery.prototype.toggle,showSpeed:"fast"};_.extend(this.HUAVOptions,m||{});this.HUAVOptions.hasBeenShown=this.HUAVOptions.$elementShown.is(":visible");this.hidden=this.isHidden();if(k){var l=this;k.on("click",function(n){l.toggle(l.HUAVOptions.showSpeed)})}},isHidden:function(){return(this.HUAVOptions.$elementShown.is(":hidden"))},toggle:function(){if(this.hidden){if(!this.HUAVOptions.hasBeenShown){if(_.isFunction(this.HUAVOptions.onshowFirstTime)){this.HUAVOptions.hasBeenShown=true;this.HUAVOptions.onshowFirstTime.call(this)}}if(_.isFunction(this.HUAVOptions.onshow)){this.HUAVOptions.onshow.call(this);this.trigger("hiddenUntilActivated:shown",this)}this.hidden=false}else{if(_.isFunction(this.HUAVOptions.onhide)){this.HUAVOptions.onhide.call(this);this.trigger("hiddenUntilActivated:hidden",this)}this.hidden=true}return this.HUAVOptions.showFn.apply(this.HUAVOptions.$elementShown,arguments)}};var f={initialize:function(k){this.draggable=k.draggable||false},$dragHandle:function(){return this.$(".title-bar")},toggleDraggable:function(){if(this.draggable){this.draggableOff()}else{this.draggableOn()}},draggableOn:function(){this.draggable=true;this.dragStartHandler=_.bind(this._dragStartHandler,this);this.dragEndHandler=_.bind(this._dragEndHandler,this);var k=this.$dragHandle().attr("draggable",true).get(0);k.addEventListener("dragstart",this.dragStartHandler,false);k.addEventListener("dragend",this.dragEndHandler,false)},draggableOff:function(){this.draggable=false;var k=this.$dragHandle().attr("draggable",false).get(0);k.removeEventListener("dragstart",this.dragStartHandler,false);k.removeEventListener("dragend",this.dragEndHandler,false)},_dragStartHandler:function(k){this.trigger("dragstart",this);k.dataTransfer.effectAllowed="move";k.dataTransfer.setData("text",JSON.stringify(this.model.toJSON()));return false},_dragEndHandler:function(k){this.trigger("dragend",this);return false}};var d={initialize:function(k){this.selectable=k.selectable||false;this.selected=k.selected||false},$selector:function(){return this.$(".selector")},_renderSelected:function(){this.$selector().find("span").toggleClass("fa-check-square-o",this.selected).toggleClass("fa-square-o",!this.selected)},toggleSelector:function(){if(!this.$selector().is(":visible")){this.showSelector()}else{this.hideSelector()}},showSelector:function(k){k=k!==undefined?k:this.fxSpeed;this.selectable=true;this.trigger("selectable",true,this);this._renderSelected();this.$selector().show(k)},hideSelector:function(k){k=k!==undefined?k:this.fxSpeed;this.selectable=false;this.trigger("selectable",false,this);this.$selector().hide(k)},toggleSelect:function(k){if(this.selected){this.deselect(k)}else{this.select(k)}},select:function(k){if(!this.selected){this.trigger("selected",this,k);this.selected=true;this._renderSelected()}return false},deselect:function(k){if(this.selected){this.trigger("de-selected",this,k);this.selected=false;this._renderSelected()}return false}};function e(l,k){k=k||"model";var m=_.template(l.join(""));return function(o,n){var p={view:n||{},_l:b};p[k]=o||{};return m(p)}}return{LoggableMixin:i,SessionStorageModel:a,mixin:j,SearchableModelMixin:g,HiddenUntilActivatedViewMixin:c,DraggableViewMixin:f,SelectableViewMixin:d,wrapTemplate:e}});
\ No newline at end of file
diff --git a/static/scripts/packed/mvc/collection/collection-li.js b/static/scripts/packed/mvc/collection/collection-li.js
index f07aab4f9f9..18cba771760 100644
--- a/static/scripts/packed/mvc/collection/collection-li.js
+++ b/static/scripts/packed/mvc/collection/collection-li.js
@@ -1 +1 @@
-define(["mvc/dataset/dataset-li","mvc/base-mvc","utils/localization"],function(h,d,f){var c=d.ListItemView;var a=c.extend({className:c.prototype.className+" dataset-collection",id:function(){return["dataset_collection",this.model.get("id")].join("-")},fxSpeed:"fast",initialize:function(i){if(i.logger){this.logger=this.model.logger=i.logger}this.log(this+"(DCListItemView).initialize:",i);c.prototype.initialize.call(this,i)},expand:function(){var i=this;return i._fetchModelDetails().always(function(){i.trigger("expanded",i)})},_renderSubtitle:function(){var i=$('');switch(this.model.get("collection_type")){case"list":return i.text(f("a list of datasets"));case"paired":return i.text(f("a pair of datasets"));case"list:paired":return i.text(f("a list of paired datasets"))}return i},toString:function(){var i=(this.model)?(this.model+""):("(no model)");return"DCListItemView("+i+")"}});a.prototype.templates=(function(){var i=d.wrapTemplate(['
"],"element");return _.extend({},h.DatasetListItemView.prototype.templates,{titleBar:i})}());var g=a.extend({className:a.prototype.className+" dataset-collection-element",_swapNewRender:function(i){a.prototype._swapNewRender.call(this,i);var j=this.model.get("state")||"ok";this.$el.addClass("state-"+j);return this.$el},toString:function(){var i=(this.model)?(this.model+""):("(no model)");return"NestedDCDCEListItemView("+i+")"}});return{DCListItemView:a,DCEListItemView:b,DatasetDCEListItemView:e,NestedDCDCEListItemView:g}});
\ No newline at end of file
+define(["mvc/list/list-item","mvc/dataset/dataset-li","mvc/base-mvc","utils/localization"],function(a,h,i,d){var e=a.ListItemView;var c=e.extend({className:e.prototype.className+" dataset-collection",id:function(){return["dataset_collection",this.model.get("id")].join("-")},fxSpeed:"fast",initialize:function(j){if(j.logger){this.logger=this.model.logger=j.logger}this.log(this+"(DCListItemView).initialize:",j);e.prototype.initialize.call(this,j)},expand:function(){var j=this;return j._fetchModelDetails().always(function(){j.trigger("expanded",j)})},_renderSubtitle:function(){var j=$('');switch(this.model.get("collection_type")){case"list":return j.text(d("a list of datasets"));case"paired":return j.text(d("a pair of datasets"));case"list:paired":return j.text(d("a list of paired datasets"))}return j},toString:function(){var j=(this.model)?(this.model+""):("(no model)");return"DCListItemView("+j+")"}});c.prototype.templates=(function(){var j=i.wrapTemplate(['
","<% } %>"],"dataset")});return _.extend({},d.prototype.templates,{titleBar:g,warnings:f})}());return{HDAListItemView:b}});
\ No newline at end of file
+define(["mvc/dataset/dataset-li","mvc/base-mvc","utils/localization"],function(e,a,c){var d=e.DatasetListItemView;var b=d.extend({className:d.prototype.className+" history-content",initialize:function(f,g){d.prototype.initialize.call(this,f,g)},toString:function(){var f=(this.model)?(this.model+""):("(no model)");return"HDAListItemView("+f+")"}});b.prototype.templates=(function(){var g=a.wrapTemplate(['
").addClass("additional-info").text(f)]);this.$datasetsList(g).append(h)},events:_.extend(_.clone(d.prototype.events),{"click tr":function(f){$(f.currentTarget).find(".dataset-title-bar").click()},"click .icon-btn":function(f){f.stopPropagation()}}),toString:function(){return"AnnotatedHistoryPanel("+((this.model)?(this.model.get("name")):(""))+")"}});return{AnnotatedHistoryPanel:c}});
\ No newline at end of file
+define(["mvc/history/history-panel","mvc/history/hda-li","mvc/history/hdca-li","mvc/base-mvc","utils/localization"],function(f,c,g,a,b){var e=f.HistoryPanel;var d=e.extend({className:e.prototype.className+" annotated-history-panel",_buildNewRender:function(){var h=e.prototype._buildNewRender.call(this);this.renderHistoryAnnotation(h);return h},renderHistoryAnnotation:function(i){var h=this.model.get("annotation");if(!h){return}i.find(".controls .annotation-display").text(h)},renderItems:function(i){i=i||this.$el;i.find(".list-items").replaceWith($("
").addClass("additional-info").text(i)]);return k}));return this},events:_.extend(_.clone(e.prototype.events),{"click tr":function(h){$(h.currentTarget).find(".title-bar").click()},"click .icon-btn":function(h){h.stopPropagation()}}),toString:function(){return"AnnotatedHistoryPanel("+((this.model)?(this.model.get("name")):(""))+")"}});return{AnnotatedHistoryPanel:d}});
\ No newline at end of file
diff --git a/static/scripts/packed/mvc/history/history-panel-edit-current.js b/static/scripts/packed/mvc/history/history-panel-edit-current.js
index 214b31fdac3..1ea9ce9eca3 100644
--- a/static/scripts/packed/mvc/history/history-panel-edit-current.js
+++ b/static/scripts/packed/mvc/history/history-panel-edit-current.js
@@ -1 +1 @@
-define(["mvc/history/history-model","mvc/history/history-panel-edit","mvc/collection/collection-panel","mvc/base-mvc","utils/localization"],function(d,c,a,h,b){var e=h.SessionStorageModel.extend({defaults:{searching:false,tagsEditorShown:false,annotationEditorShown:false},toString:function(){return"HistoryPanelPrefs("+JSON.stringify(this.toJSON())+")"}});e.storageKey=function f(){return("history-panel")};var g=c.HistoryPanel;var i=g.extend({emptyMsg:b("This history is empty. Click 'Get Data' on the left tool menu to start"),noneFoundMsg:b("No matching datasets found"),initialize:function(j){j=j||{};this.preferences=new e(_.extend({id:e.storageKey()},_.pick(j,_.keys(e.prototype.defaults))));g.prototype.initialize.call(this,j);this.panelStack=[]},loadCurrentHistory:function(k){this.debug(this+".loadCurrentHistory");var j=this;return this.loadHistoryWithHDADetails("current",k).then(function(m,l){j.trigger("current-history",j)})},switchToHistory:function(m,l){var j=this,k=function(){return jQuery.getJSON(galaxy_config.root+"history/set_as_current?id="+m)};return this.loadHistoryWithHDADetails(m,l,k).then(function(o,n){j.trigger("switched-history",j)})},createNewHistory:function(l){if(!Galaxy||!Galaxy.currUser||Galaxy.currUser.isAnonymous()){this.displayMessage("error",b("You must be logged in to create histories"));return $.when()}var j=this,k=function(){return jQuery.getJSON(galaxy_config.root+"history/create_new_current")};return this.loadHistory(undefined,l,k).then(function(n,m){j.trigger("new-history",j)})},setModel:function(k,j,l){g.prototype.setModel.call(this,k,j,l);if(this.model){this.log("checking for updates");this.model.checkForUpdates()}return this},_setUpModelEventHandlers:function(){g.prototype._setUpModelEventHandlers.call(this);if(Galaxy&&Galaxy.quotaMeter){this.listenTo(this.model,"change:nice_size",function(){Galaxy.quotaMeter.update()})}this.model.contents.on("state:ready",function(k,l,j){if((!k.get("visible"))&&(!this.storage.get("show_hidden"))){this.removeHdaView(this.hdaViews[k.id])}},this)},render:function(l,m){this.log("render:",l,m);l=(l===undefined)?(this.fxSpeed):(l);var j=this,k;if(this.model){k=this.renderModel()}else{k=this.renderWithoutModel()}$(j).queue("fx",[function(n){if(l&&j.$el.is(":visible")){j.$el.fadeOut(l,n)}else{n()}},function(n){j.$el.empty();if(k){j.$el.append(k.children());j.renderBasedOnPrefs()}n()},function(n){if(l&&!j.$el.is(":visible")){j.$el.fadeIn(l,n)}else{n()}},function(n){if(m){m.call(this)}j.trigger("rendered",this);n()}]);return this},renderBasedOnPrefs:function(){if(this.preferences.get("searching")){this.toggleSearchControls(0,true)}},_renderEmptyMsg:function(l){var k=this,j=k.$emptyMessage(l),m=$(".toolMenuContainer");if((_.isEmpty(k.hdaViews)&&!k.searchFor)&&(Galaxy&&Galaxy.upload&&m.size())){j.empty();j.html([b("This history is empty"),". ",b("You can "),'',b("load your own data"),"",b(" or "),'',b("get data from an external source"),""].join(""));j.find(".uploader-link").click(function(n){Galaxy.upload._eventShow(n)});j.find(".get-data-link").click(function(n){m.parent().scrollTop(0);m.find('span:contains("Get Data")').click()});j.show()}else{g.prototype._renderEmptyMsg.call(this,l)}return this},toggleSearchControls:function(k,j){var l=g.prototype.toggleSearchControls.call(this,k,j);this.preferences.set("searching",l)},_renderTags:function(j){var k=this;g.prototype._renderTags.call(this,j);if(this.preferences.get("tagsEditorShown")){this.tagsEditor.toggle(true)}this.tagsEditor.on("hiddenUntilActivated:shown hiddenUntilActivated:hidden",function(l){k.preferences.set("tagsEditorShown",l.hidden)})},_renderAnnotation:function(j){var k=this;g.prototype._renderAnnotation.call(this,j);if(this.preferences.get("annotationEditorShown")){this.annotationEditor.toggle(true)}this.annotationEditor.on("hiddenUntilActivated:shown hiddenUntilActivated:hidden",function(l){k.preferences.set("annotationEditorShown",l.hidden)})},_setUpHdaListeners:function(k){g.prototype._setUpHdaListeners.call(this,k);var j=this;if(k instanceof j.HDCAViewClass){k.off("expanded");k.on("expanded",function(l){this.info("expanded",l);j._addCollectionPanel(l)});k.off("collapsed")}},_addCollectionPanel:function(l){var m=this,j=l.model;this.debug("history panel (stack), collectionModel:",j);var k=new (this._getCollectionPanelClass(j))({model:j,HDAViewClass:this.HDAViewClass,parentName:this.model.get("name"),linkTarget:this.linkTarget});m.panelStack.push(k);m.$(".history-controls").add(".datasets-list").hide();m.$el.append(k.$el);k.on("close",function(){m.render();l.collapse();m.panelStack.pop()});if(!k.model.hasDetails()){var n=k.model.fetch();n.done(function(){k.render()})}else{k.render()}},_getCollectionPanelClass:function(j){switch(j.get("collection_type")){case"list":return a.ListCollectionPanel;case"paired":return a.PairCollectionPanel;case"list:paired":return a.ListOfPairsCollectionPanel}throw new TypeError("Uknown collection_type: "+j.get("collection_type"))},addContentView:function(j){if(this.panelStack.length){return this}return g.prototype.addContentView.call(this,j)},connectToQuotaMeter:function(j){if(!j){return this}this.listenTo(j,"quota:over",this.showQuotaMessage);this.listenTo(j,"quota:under",this.hideQuotaMessage);this.on("rendered rendered:initial",function(){if(j&&j.isOverQuota()){this.showQuotaMessage()}});return this},showQuotaMessage:function(){var j=this.$el.find(".quota-message");if(j.is(":hidden")){j.slideDown(this.fxSpeed)}},hideQuotaMessage:function(){var j=this.$el.find(".quota-message");if(!j.is(":hidden")){j.slideUp(this.fxSpeed)}},connectToOptionsMenu:function(j){if(!j){return this}this.on("new-storage",function(l,k){if(j&&l){j.findItemByHtml(b("Include Deleted Datasets")).checked=l.get("show_deleted");j.findItemByHtml(b("Include Hidden Datasets")).checked=l.get("show_hidden")}});return this},toString:function(){return"CurrentHistoryPanel("+((this.model)?(this.model.get("name")):(""))+")"}});return{CurrentHistoryPanel:i}});
\ No newline at end of file
+define(["mvc/history/history-model","mvc/history/history-panel-edit","mvc/collection/collection-panel","mvc/base-mvc","utils/localization"],function(d,c,a,h,b){var e=h.SessionStorageModel.extend({defaults:{tagsEditorShown:false,annotationEditorShown:false},toString:function(){return"HistoryPanelPrefs("+JSON.stringify(this.toJSON())+")"}});e.storageKey=function f(){return("history-panel")};var g=c.HistoryPanelEdit;var i=g.extend({className:g.prototype.className+" current-history-panel",emptyMsg:b("This history is empty. Click 'Get Data' on the left tool menu to start"),noneFoundMsg:b("No matching datasets found"),initialize:function(j){j=j||{};this.preferences=new e(_.extend({id:e.storageKey()},_.pick(j,_.keys(e.prototype.defaults))));g.prototype.initialize.call(this,j);this.panelStack=[]},loadCurrentHistory:function(k){this.debug(this+".loadCurrentHistory");var j=this;return this.loadHistoryWithDetails("current",k).then(function(m,l){j.trigger("current-history",j)})},switchToHistory:function(m,l){var j=this,k=function(){return jQuery.getJSON(galaxy_config.root+"history/set_as_current?id="+m)};return this.loadHistoryWithDetails(m,l,k).then(function(o,n){j.trigger("switched-history",j)})},createNewHistory:function(l){if(!Galaxy||!Galaxy.currUser||Galaxy.currUser.isAnonymous()){this.displayMessage("error",b("You must be logged in to create histories"));return $.when()}var j=this,k=function(){return jQuery.getJSON(galaxy_config.root+"history/create_new_current")};return this.loadHistory(undefined,l,k).then(function(n,m){j.trigger("new-history",j)})},setModel:function(k,j,l){g.prototype.setModel.call(this,k,j,l);if(this.model){this.log("checking for updates");this.model.checkForUpdates()}return this},_setUpCollectionListeners:function(){g.prototype._setUpCollectionListeners.call(this);this.collection.on("state:ready",function(k,l,j){if((!k.get("visible"))&&(!this.storage.get("show_hidden"))){this.removeItemView(this.viewFromModel(k))}},this)},_setUpModelListeners:function(){g.prototype._setUpModelListeners.call(this);if(Galaxy&&Galaxy.quotaMeter){this.listenTo(this.model,"change:nice_size",function(){Galaxy.quotaMeter.update()})}},_buildNewRender:function(){if(!this.model){return $()}var j=g.prototype._buildNewRender.call(this);j.find(".search").prependTo(j.find(".controls"));this._renderQuotaMessage(j);return j},_renderQuotaMessage:function(j){j=j||this.$el;return $(this.templates.quotaMsg({},this)).prependTo(j.find(".messages"))},_renderEmptyMessage:function(l){var k=this,j=k.$emptyMessage(l),m=$(".toolMenuContainer");if((_.isEmpty(k.views)&&!k.searchFor)&&(Galaxy&&Galaxy.upload&&m.size())){j.empty();j.html([b("This history is empty"),". ",b("You can "),'',b("load your own data"),"",b(" or "),'',b("get data from an external source"),""].join(""));j.find(".uploader-link").click(function(n){Galaxy.upload._eventShow(n)});j.find(".get-data-link").click(function(n){m.parent().scrollTop(0);m.find('span:contains("Get Data")').click()});return j.show()}return g.prototype._renderEmptyMessage.call(this,l)},_renderTags:function(j){var k=this;g.prototype._renderTags.call(this,j);if(this.preferences.get("tagsEditorShown")){this.tagsEditor.toggle(true)}this.tagsEditor.on("hiddenUntilActivated:shown hiddenUntilActivated:hidden",function(l){k.preferences.set("tagsEditorShown",l.hidden)})},_renderAnnotation:function(j){var k=this;g.prototype._renderAnnotation.call(this,j);if(this.preferences.get("annotationEditorShown")){this.annotationEditor.toggle(true)}this.annotationEditor.on("hiddenUntilActivated:shown hiddenUntilActivated:hidden",function(l){k.preferences.set("annotationEditorShown",l.hidden)})},_attachItems:function(j){this.$list(j).append(this.views.reverse().map(function(k){return k.$el}));return this},_setUpItemViewListeners:function(k){var j=this;g.prototype._setUpItemViewListeners.call(j,k);if(k instanceof j.HDCAViewClass){k.off("expanded");k.on("expanded",function(l){j.info("expanded",l);j._addCollectionPanel(l)});k.off("collapsed")}},_addCollectionPanel:function(l){var k=this,j=l.model;this.debug("history panel (stack), collectionModel:",j);var n=new (this._getCollectionPanelClass(j))({model:j,HDAViewClass:this.HDAViewClass,parentName:this.model.get("name"),linkTarget:this.linkTarget});k.panelStack.push(n);k.$(".controls").add(k.$list()).hide();k.$el.append(n.$el);n.on("close",function(){k.render();l.collapse();k.panelStack.pop()});if(!n.model.hasDetails()){var m=n.model.fetch();m.done(function(){n.render()})}else{n.render()}},_getCollectionPanelClass:function(j){switch(j.get("collection_type")){case"list":return a.ListCollectionPanel;case"paired":return a.PairCollectionPanel;case"list:paired":return a.ListOfPairsCollectionPanel}throw new TypeError("Uknown collection_type: "+j.get("collection_type"))},addItemView:function(o,p,n){this.log(this+".addItemView:",o);if(!this._filterItem(o)){return undefined}var l=this;if(this.panelStack.length){return l}var k=l._createItemView(o);this.views.unshift(k);l.scrollToTop();$({}).queue([function m(r){var q=l.$emptyMessage();if(q.is(":visible")){q.fadeOut(l.fxSpeed,r)}else{r()}},function j(q){l.scrollToTop();l.$list().prepend(k.render(0).$el.hide());k.$el.slideDown(l.fxSpeed)}]);return k},connectToQuotaMeter:function(j){if(!j){return this}this.listenTo(j,"quota:over",this.showQuotaMessage);this.listenTo(j,"quota:under",this.hideQuotaMessage);this.on("rendered rendered:initial",function(){if(j&&j.isOverQuota()){this.showQuotaMessage()}});return this},showQuotaMessage:function(){var j=this.$el.find(".quota-message");if(j.is(":hidden")){j.slideDown(this.fxSpeed)}},hideQuotaMessage:function(){var j=this.$el.find(".quota-message");if(!j.is(":hidden")){j.slideUp(this.fxSpeed)}},connectToOptionsMenu:function(j){if(!j){return this}this.on("new-storage",function(l,k){if(j&&l){j.findItemByHtml(b("Include Deleted Datasets")).checked=l.get("show_deleted");j.findItemByHtml(b("Include Hidden Datasets")).checked=l.get("show_hidden")}});return this},toString:function(){return"CurrentHistoryPanel("+((this.model)?(this.model.get("name")):(""))+")"}});i.prototype.templates=(function(){var j=h.wrapTemplate(['
',b("You are over your disk quota"),". ",b("Tool execution is on hold until your disk usage drops below your allocated quota"),".","
"],"history");return _.extend(_.clone(g.prototype.templates),{quotaMsg:j})}());return{CurrentHistoryPanel:i}});
\ No newline at end of file
diff --git a/static/scripts/packed/mvc/history/history-panel-edit.js b/static/scripts/packed/mvc/history/history-panel-edit.js
index 7371256cdb1..1dbeda9c813 100644
--- a/static/scripts/packed/mvc/history/history-panel-edit.js
+++ b/static/scripts/packed/mvc/history/history-panel-edit.js
@@ -1 +1 @@
-define(["mvc/history/history-panel","mvc/history/history-contents","mvc/dataset/states","mvc/history/hda-model","mvc/history/hda-li-edit","mvc/history/hdca-li-edit","mvc/tags","mvc/annotations","utils/localization"],function(f,h,k,d,c,g,j,a,b){var i=f.ReadOnlyHistoryPanel;var e=i.extend({HDAViewClass:c.HDAListItemEdit,HDCAViewClass:g.HDCAListItemEdit,initialize:function(l){l=l||{};this.selectedHdaIds=[];this.lastSelectedViewId=null;this.tagsEditor=null;this.annotationEditor=null;this.purgeAllowed=l.purgeAllowed||false;this.selecting=l.selecting||false;this.annotationEditorShown=l.annotationEditorShown||false;this.tagsEditorShown=l.tagsEditorShown||false;i.prototype.initialize.call(this,l)},_setUpModelEventHandlers:function(){i.prototype._setUpModelEventHandlers.call(this);this.model.on("change:nice_size",this.updateHistoryDiskSize,this);this.model.contents.on("change:deleted",this._handleHdaDeletionChange,this);this.model.contents.on("change:visible",this._handleHdaVisibleChange,this);this.model.contents.on("change:purged",function(l){this.model.fetch()},this)},renderModel:function(){var l=$("");l.append(e.templates.historyPanel(this.model.toJSON()));this.$emptyMessage(l).text(this.emptyMsg);if(Galaxy&&Galaxy.currUser&&Galaxy.currUser.id&&Galaxy.currUser.id===this.model.get("user_id")){this._renderTags(l);this._renderAnnotation(l)}l.find(".history-secondary-actions").prepend(this._renderSelectButton());l.find(".history-dataset-actions").toggle(this.selecting);l.find(".history-secondary-actions").prepend(this._renderSearchButton());this._setUpBehaviours(l);this.renderHdas(l);return l},_renderTags:function(l){var m=this;this.tagsEditor=new j.TagsEditor({model:this.model,el:l.find(".history-controls .tags-display"),onshowFirstTime:function(){this.render()},onshow:function(){m.toggleHDATagEditors(true,m.fxSpeed)},onhide:function(){m.toggleHDATagEditors(false,m.fxSpeed)},$activator:faIconButton({title:b("Edit history tags"),classes:"history-tag-btn",faIcon:"fa-tags"}).appendTo(l.find(".history-secondary-actions"))})},_renderAnnotation:function(l){var m=this;this.annotationEditor=new a.AnnotationEditor({model:this.model,el:l.find(".history-controls .annotation-display"),onshowFirstTime:function(){this.render()},onshow:function(){m.toggleHDAAnnotationEditors(true,m.fxSpeed)},onhide:function(){m.toggleHDAAnnotationEditors(false,m.fxSpeed)},$activator:faIconButton({title:b("Edit history annotation"),classes:"history-annotate-btn",faIcon:"fa-comment"}).appendTo(l.find(".history-secondary-actions"))})},_renderSelectButton:function(l){return faIconButton({title:b("Operations on multiple datasets"),classes:"history-select-btn",faIcon:"fa-check-square-o"})},_setUpBehaviours:function(l){l=l||this.$el;i.prototype._setUpBehaviours.call(this,l);if(!this.model){return}this.actionsPopup=this._setUpDatasetActionsPopup(l);if((!Galaxy.currUser||Galaxy.currUser.isAnonymous())||(Galaxy.currUser.id!==this.model.get("user_id"))){return}var m=this;l.find(".history-name").attr("title",b("Click to rename history")).tooltip({placement:"bottom"}).make_text_editable({on_finish:function(n){var o=m.model.get("name");if(n&&n!==o){m.$el.find(".history-name").text(n);m.model.save({name:n}).fail(function(){m.$el.find(".history-name").text(m.model.previous("name"))})}else{m.$el.find(".history-name").text(o)}}})},_setUpDatasetActionsPopup:function(l){var m=this,n=[{html:b("Hide datasets"),func:function(){var o=d.HistoryDatasetAssociation.prototype.hide;m.getSelectedHdaCollection().ajaxQueue(o)}},{html:b("Unhide datasets"),func:function(){var o=d.HistoryDatasetAssociation.prototype.unhide;m.getSelectedHdaCollection().ajaxQueue(o)}},{html:b("Delete datasets"),func:function(){var o=d.HistoryDatasetAssociation.prototype["delete"];m.getSelectedHdaCollection().ajaxQueue(o)}},{html:b("Undelete datasets"),func:function(){var o=d.HistoryDatasetAssociation.prototype.undelete;m.getSelectedHdaCollection().ajaxQueue(o)}}];if(m.purgeAllowed){n.push({html:b("Permanently delete datasets"),func:function(){if(confirm(b("This will permanently remove the data in your datasets. Are you sure?"))){var o=d.HistoryDatasetAssociation.prototype.purge;m.getSelectedHdaCollection().ajaxQueue(o)}}})}n.push({html:b("Build Dataset List (Experimental)"),func:function(){m.getSelectedHdaCollection().promoteToHistoryDatasetCollection(m.model,"list")}});n.push({html:b("Build Dataset Pair (Experimental)"),func:function(){m.getSelectedHdaCollection().promoteToHistoryDatasetCollection(m.model,"paired")}});n.push({html:b("Build List of Dataset Pairs (Experimental)"),func:_.bind(m._showPairedCollectionModal,m)});return new PopupMenu(l.find(".history-dataset-action-popup-btn"),n)},_showPairedCollectionModal:function(){var l=this,m=l.getSelectedHdaCollection().toJSON().filter(function(n){return n.history_content_type==="dataset"&&n.state===k.OK});if(m.length){require(["mvc/collection/paired-collection-creator"],function(n){window.creator=n.pairedCollectionCreatorModal(m,{historyId:l.model.id})})}else{}},_handleHdaDeletionChange:function(l){if(l.get("deleted")&&!this.storage.get("show_deleted")){this.removeHdaView(this.hdaViews[l.id])}},_handleHdaVisibleChange:function(l){if(l.hidden()&&!this.storage.get("show_hidden")){this.removeHdaView(this.hdaViews[l.id])}},_getContentOptions:function(m){var l=i.prototype._getContentOptions.call(this,m);_.extend(l,{selectable:this.selecting,purgeAllowed:this.purgeAllowed,tagsEditorShown:(this.tagsEditor&&!this.tagsEditor.hidden),annotationEditorShown:(this.annotationEditor&&!this.annotationEditor.hidden)});return l},_setUpHdaListeners:function(m){var l=this;i.prototype._setUpHdaListeners.call(this,m);m.on("selected",function(p,o){if(!o){return}var n=[];if((o.shiftKey)&&(l.lastSelectedViewId&&_.has(l.hdaViews,l.lastSelectedViewId))){var r=l.hdaViews[l.lastSelectedViewId];n=l.selectDatasetRange(p,r).map(function(s){return s.model.id})}else{var q=p.model.id;l.lastSelectedViewId=q;n=[q]}l.selectedHdaIds=_.union(l.selectedHdaIds,n)});m.on("de-selected",function(o,n){var p=o.model.id;l.selectedHdaIds=_.without(l.selectedHdaIds,p)})},toggleHDATagEditors:function(l){var m=arguments;_.each(this.hdaViews,function(n){if(n.tagsEditor){n.tagsEditor.toggle.apply(n.tagsEditor,m)}})},toggleHDAAnnotationEditors:function(l){var m=arguments;_.each(this.hdaViews,function(n){if(n.annotationEditor){n.annotationEditor.toggle.apply(n.annotationEditor,m)}})},removeHdaView:function(m){if(!m){return}var l=this;m.$el.fadeOut(l.fxSpeed,function(){m.off();m.remove();delete l.hdaViews[m.model.id];if(_.isEmpty(l.hdaViews)){l.trigger("empty-history",l);l._renderEmptyMsg()}})},events:_.extend(_.clone(i.prototype.events),{"click .history-select-btn":"toggleSelectors","click .history-select-all-datasets-btn":"selectAllDatasets","click .history-deselect-all-datasets-btn":"deselectAllDatasets"}),updateHistoryDiskSize:function(){this.$el.find(".history-size").text(this.model.get("nice_size"))},showSelectors:function(l){l=(l!==undefined)?(l):(this.fxSpeed);this.selecting=true;this.$(".history-dataset-actions").slideDown(l);_.each(this.hdaViews,function(m){m.showSelector()});this.selectedHdaIds=[];this.lastSelectedViewId=null},hideSelectors:function(l){l=(l!==undefined)?(l):(this.fxSpeed);this.selecting=false;this.$(".history-dataset-actions").slideUp(l);_.each(this.hdaViews,function(m){m.hideSelector()});this.selectedHdaIds=[];this.lastSelectedViewId=null},toggleSelectors:function(){if(!this.selecting){this.showSelectors()}else{this.hideSelectors()}},selectAllDatasets:function(l){_.each(this.hdaViews,function(m){m.select(l)})},deselectAllDatasets:function(l){this.lastSelectedViewId=null;_.each(this.hdaViews,function(m){m.deselect(l)})},selectDatasetRange:function(n,m){var l=this.hdaViewRange(n,m);_.each(l,function(o){o.select()});return l},getSelectedHdaViews:function(){return _.filter(this.hdaViews,function(l){return l.selected})},getSelectedHdaCollection:function(){return new h.HistoryContents(_.map(this.getSelectedHdaViews(),function(l){return l.model}),{historyId:this.model.id})},toString:function(){return"HistoryPanel("+((this.model)?(this.model.get("name")):(""))+")"}});return{HistoryPanel:e}});
\ No newline at end of file
+define(["mvc/history/history-panel","mvc/history/history-contents","mvc/dataset/states","mvc/history/hda-model","mvc/history/hda-li-edit","mvc/history/hdca-li-edit","mvc/tags","mvc/annotations","utils/localization"],function(f,h,k,d,c,g,j,a,b){var i=f.HistoryPanel;var e=i.extend({HDAViewClass:c.HDAListItemEdit,HDCAViewClass:g.HDCAListItemEdit,initialize:function(l){l=l||{};i.prototype.initialize.call(this,l);this.tagsEditor=null;this.annotationEditor=null;this.purgeAllowed=l.purgeAllowed||false;this.annotationEditorShown=l.annotationEditorShown||false;this.tagsEditorShown=l.tagsEditorShown||false;this.multiselectActions=l.multiselectActions||this._getActions()},_setUpCollectionListeners:function(){i.prototype._setUpCollectionListeners.call(this);this.collection.on("change:deleted",this._handleHdaDeletionChange,this);this.collection.on("change:visible",this._handleHdaVisibleChange,this);this.collection.on("change:purged",function(l){this.model.fetch()},this);return this},_setUpModelListeners:function(){i.prototype._setUpModelListeners.call(this);this.model.on("change:nice_size",this.updateHistoryDiskSize,this);return this},_buildNewRender:function(){var l=i.prototype._buildNewRender.call(this);if(!this.model){return l}if(Galaxy&&Galaxy.currUser&&Galaxy.currUser.id&&Galaxy.currUser.id===this.model.get("user_id")){this._renderTags(l);this._renderAnnotation(l)}return l},_renderTags:function(l){var m=this;this.tagsEditor=new j.TagsEditor({model:this.model,el:l.find(".controls .tags-display"),onshowFirstTime:function(){this.render()},onshow:function(){m.toggleHDATagEditors(true,m.fxSpeed)},onhide:function(){m.toggleHDATagEditors(false,m.fxSpeed)},$activator:faIconButton({title:b("Edit history tags"),classes:"history-tag-btn",faIcon:"fa-tags"}).appendTo(l.find(".controls .actions"))})},_renderAnnotation:function(l){var m=this;this.annotationEditor=new a.AnnotationEditor({model:this.model,el:l.find(".controls .annotation-display"),onshowFirstTime:function(){this.render()},onshow:function(){m.toggleHDAAnnotationEditors(true,m.fxSpeed)},onhide:function(){m.toggleHDAAnnotationEditors(false,m.fxSpeed)},$activator:faIconButton({title:b("Edit history annotation"),classes:"history-annotate-btn",faIcon:"fa-comment"}).appendTo(l.find(".controls .actions"))})},_setUpBehaviors:function(l){l=l||this.$el;i.prototype._setUpBehaviors.call(this,l);if(!this.model){return}if(this.multiselectActions.length){this.actionsPopup=new PopupMenu(l.find(".list-action-popup-btn"),this.multiselectActions)}if((!Galaxy.currUser||Galaxy.currUser.isAnonymous())||(Galaxy.currUser.id!==this.model.get("user_id"))){return}var m=this,n=".controls .name";l.find(n).attr("title",b("Click to rename history")).tooltip({placement:"bottom"}).make_text_editable({on_finish:function(o){var p=m.model.get("name");if(o&&o!==p){m.$el.find(n).text(o);m.model.save({name:o}).fail(function(){m.$el.find(n).text(m.model.previous("name"))})}else{m.$el.find(n).text(p)}}})},_getActions:function(){var l=this,m=[{html:b("Hide datasets"),func:function(){var n=d.HistoryDatasetAssociation.prototype.hide;l.getSelectedModels().ajaxQueue(n)}},{html:b("Unhide datasets"),func:function(){var n=d.HistoryDatasetAssociation.prototype.unhide;l.getSelectedModels().ajaxQueue(n)}},{html:b("Delete datasets"),func:function(){var n=d.HistoryDatasetAssociation.prototype["delete"];l.getSelectedModels().ajaxQueue(n)}},{html:b("Undelete datasets"),func:function(){var n=d.HistoryDatasetAssociation.prototype.undelete;l.getSelectedModels().ajaxQueue(n)}}];if(l.purgeAllowed){m.push({html:b("Permanently delete datasets"),func:function(){if(confirm(b("This will permanently remove the data in your datasets. Are you sure?"))){var n=d.HistoryDatasetAssociation.prototype.purge;l.getSelectedModels().ajaxQueue(n)}}})}m.push({html:b("Build Dataset List"),func:function(){l.getSelectedModels().promoteToHistoryDatasetCollection(l.model,"list")}});m.push({html:b("Build Dataset Pair"),func:function(){l.getSelectedModels().promoteToHistoryDatasetCollection(l.model,"paired")}});m.push({html:b("Build List of Dataset Pairs"),func:_.bind(l._showPairedCollectionModal,l)});return m},_showPairedCollectionModal:function(){var l=this,m=l.getSelectedModels().toJSON().filter(function(n){return n.history_content_type==="dataset"&&n.state===k.OK});if(m.length){require(["mvc/collection/paired-collection-creator"],function(n){window.creator=n.pairedCollectionCreatorModal(m,{historyId:l.model.id})})}else{}},_getItemViewOptions:function(m){var l=i.prototype._getItemViewOptions.call(this,m);_.extend(l,{purgeAllowed:this.purgeAllowed,tagsEditorShown:(this.tagsEditor&&!this.tagsEditor.hidden),annotationEditorShown:(this.annotationEditor&&!this.annotationEditor.hidden)});return l},_handleHdaDeletionChange:function(l){if(l.get("deleted")&&!this.storage.get("show_deleted")){this.removeItemView(l)}},_handleHdaVisibleChange:function(l){if(l.hidden()&&!this.storage.get("show_hidden")){this.removeItemView(l)}},toggleHDATagEditors:function(l){var m=Array.prototype.slice.call(arguments,1);_.each(this.views,function(n){if(n.tagsEditor){n.tagsEditor.toggle.apply(n.tagsEditor,m)}})},toggleHDAAnnotationEditors:function(l){var m=Array.prototype.slice.call(arguments,1);_.each(this.views,function(n){if(n.annotationEditor){n.annotationEditor.toggle.apply(n.annotationEditor,m)}})},events:_.extend(_.clone(i.prototype.events),{"click .show-selectors-btn":"toggleSelectors"}),updateHistoryDiskSize:function(){this.$el.find(".history-size").text(this.model.get("nice_size"))},toString:function(){return"HistoryPanelEdit("+((this.model)?(this.model.get("name")):(""))+")"}});return{HistoryPanelEdit:e}});
\ No newline at end of file
diff --git a/static/scripts/packed/mvc/history/history-panel.js b/static/scripts/packed/mvc/history/history-panel.js
index b94a54abe64..e278f6d5058 100644
--- a/static/scripts/packed/mvc/history/history-panel.js
+++ b/static/scripts/packed/mvc/history/history-panel.js
@@ -1 +1 @@
-define(["mvc/history/history-model","mvc/history/hda-li","mvc/history/hdca-li","mvc/user/user-model","mvc/base-mvc","utils/localization"],function(e,b,a,f,k,d){var i=k.SessionStorageModel.extend({defaults:{expandedHdas:{},show_deleted:false,show_hidden:false},addExpandedHda:function(m){var n="expandedHdas";this.save(n,_.extend(this.get(n),_.object([m.id],[m.get("id")])))},removeExpandedHda:function(m){var n="expandedHdas";this.save(n,_.omit(this.get(n),m.id))},toString:function(){return"HistoryPrefs("+this.id+")"}});i.storageKeyPrefix="history:";i.historyStorageKey=function g(m){if(!m){throw new Error("HistoryPrefs.historyStorageKey needs valid id: "+m)}return(i.storageKeyPrefix+m)};i.get=function c(m){return new i({id:i.historyStorageKey(m)})};i.clearAll=function h(n){for(var m in sessionStorage){if(m.indexOf(i.storageKeyPrefix)===0){sessionStorage.removeItem(m)}}};var j=Backbone.View.extend(k.LoggableMixin).extend({HDAViewClass:b.HDAListItemView,HDCAViewClass:a.HDCAListItemView,tagName:"div",className:"history-panel",fxSpeed:"fast",emptyMsg:d("This history is empty"),noneFoundMsg:d("No matching datasets found"),initialize:function(m){m=m||{};if(m.logger){this.logger=m.logger}this.log(this+".initialize:",m);this.linkTarget=m.linkTarget||"_blank";this.fxSpeed=_.has(m,"fxSpeed")?(m.fxSpeed):(this.fxSpeed);this.filters=[];this.searchFor="";this.findContainerFn=m.findContainerFn;this.hdaViews={};this.indicator=new LoadingIndicator(this.$el);this._setUpListeners();var n=_.pick(m,"initiallyExpanded","show_deleted","show_hidden");this.setModel(this.model,n,false);if(m.onready){m.onready.call(this)}},_setUpListeners:function(){this.on("error",function(n,q,m,p,o){this.errorHandler(n,q,m,p,o)});this.on("loading-history",function(){this._showLoadingIndicator("loading history...",40)});this.on("loading-done",function(){this._hideLoadingIndicator(40);if(_.isEmpty(this.hdaViews)){this.trigger("empty-history",this)}});this.once("rendered",function(){this.trigger("rendered:initial",this);return false});if(this.logger){this.on("all",function(m){this.log(this+"",arguments)},this)}return this},errorHandler:function(o,r,n,q,p){this.error(o,r,n,q,p);window.xhr=r;if(r&&r.status===0&&r.readyState===0){}else{if(r&&r.status===502){}else{var m=this._parseErrorMessage(o,r,n,q,p);if(!this.$messages().is(":visible")){this.once("rendered",function(){this.displayMessage("error",m.message,m.details)})}else{this.displayMessage("error",m.message,m.details)}}}},_parseErrorMessage:function(q,t,u,o,m,r){var p=Galaxy.currUser,s={message:this._bePolite(o),details:{message:o,raven:(window.Raven)?(Raven.lastEventId()):(undefined),agent:navigator.userAgent,url:(window.Galaxy)?(Galaxy.lastAjax.url):(undefined),data:(window.Galaxy)?(Galaxy.lastAjax.data):(undefined),options:(t)?(_.omit(u,"xhr")):(u),xhr:t,source:(_.isFunction(q.toJSON))?(q.toJSON()):(q+""),user:(p instanceof f.User)?(p.toJSON()):(p+"")}};_.extend(s.details,m||{});if(t&&_.isFunction(t.getAllResponseHeaders)){var n=t.getAllResponseHeaders();n=_.compact(n.split("\n"));n=_.map(n,function(v){return v.split(": ")});s.details.xhr.responseHeaders=_.object(n)}return s},_bePolite:function(m){m=m||d("An error occurred while getting updates from the server");return m+". "+d("Please contact a Galaxy administrator if the problem persists")+"."},loadHistoryWithHDADetails:function(o,n,m,q){this.info("loadHistoryWithHDADetails:",o,n,m,q);var p=function(r){return _.values(i.get(r.id).get("expandedHdas"))};return this.loadHistory(o,n,m,q,p)},loadHistory:function(p,o,n,s,q){this.info("loadHistory:",p,o,n,s,q);var m=this;o=o||{};m.trigger("loading-history",m);var r=e.History.getHistoryData(p,{historyFn:n,hdaFn:s,hdaDetailIds:o.initiallyExpanded||q});return m._loadHistoryFromXHR(r,o).fail(function(v,t,u){m.trigger("error",m,v,o,d("An error was encountered while "+t),{historyId:p,history:u||{}})}).always(function(){m.trigger("loading-done",m)})},_loadHistoryFromXHR:function(o,n){var m=this;o.then(function(p,q){m.JSONToModel(p,q,n)});o.fail(function(q,p){m.render()});return o},JSONToModel:function(p,m,n){this.log("JSONToModel:",p,m,n);n=n||{};var o=new e.History(p,m,n);this.setModel(o);return this},setModel:function(n,m,o){m=m||{};o=(o!==undefined)?(o):(true);this.log("setModel:",n,m,o);this.freeModel();this.selectedHdaIds=[];if(n){this.model=n;if(this.logger){this.model.logger=this.logger}this._setUpWebStorage(m.initiallyExpanded,m.show_deleted,m.show_hidden);this._setUpModelEventHandlers();this.trigger("new-model",this)}if(o){this.render()}return this},freeModel:function(){if(this.model){this.model.clearUpdateTimeout();this.stopListening(this.model);this.stopListening(this.model.contents)}this.freeHdaViews();return this},freeHdaViews:function(){this.hdaViews={};return this},_setUpWebStorage:function(n,m,o){this.storage=new i({id:i.historyStorageKey(this.model.get("id"))});if(_.isObject(n)){this.storage.set("exandedHdas",n)}if(_.isBoolean(m)){this.storage.set("show_deleted",m)}if(_.isBoolean(o)){this.storage.set("show_hidden",o)}this.trigger("new-storage",this.storage,this);this.log(this+" (init'd) storage:",this.storage.get());return this},_setUpModelEventHandlers:function(){this.model.contents.on("add",this.addContentView,this);this.model.on("error error:contents",function(n,p,m,o){this.errorHandler(n,p,m,o)},this);return this},render:function(o,p){this.log("render:",o,p);o=(o===undefined)?(this.fxSpeed):(o);var m=this,n;if(this.model){n=this.renderModel()}else{n=this.renderWithoutModel()}$(m).queue("fx",[function(q){if(o&&m.$el.is(":visible")){m.$el.fadeOut(o,q)}else{q()}},function(q){m.$el.empty();if(n){m.$el.append(n.children())}q()},function(q){if(o&&!m.$el.is(":visible")){m.$el.fadeIn(o,q)}else{q()}},function(q){if(p){p.call(this)}m.trigger("rendered",this);q()}]);return this},renderWithoutModel:function(){var m=$(""),n=$("").addClass("message-container").css({margin:"4px"});return m.append(n)},renderModel:function(){var m=$("");m.append(j.templates.historyPanel(this.model.toJSON()));this.$emptyMessage(m).text(this.emptyMsg);m.find(".history-secondary-actions").prepend(this._renderSearchButton());this._setUpBehaviours(m);this.renderHdas(m);return m},_renderEmptyMsg:function(o){var n=this,m=n.$emptyMessage(o);if(!_.isEmpty(n.hdaViews)){m.hide()}else{if(n.searchFor){m.text(n.noneFoundMsg).show()}else{m.text(n.emptyMsg).show()}}return this},_renderSearchButton:function(m){return faIconButton({title:d("Search datasets"),classes:"history-search-btn",faIcon:"fa-search"})},_setUpBehaviours:function(m){m=m||this.$el;m.find("[title]").tooltip({placement:"bottom"});this._setUpSearchInput(m.find(".history-search-controls .history-search-input"));return this},$container:function(){return(this.findContainerFn)?(this.findContainerFn.call(this)):(this.$el.parent())},$datasetsList:function(m){return(m||this.$el).find(".datasets-list")},$messages:function(m){return(m||this.$el).find(".message-container")},$emptyMessage:function(m){return(m||this.$el).find(".empty-history-message")},renderHdas:function(n){n=n||this.$el;var m=this,p={},o=this.model.contents.getVisible(this.storage.get("show_deleted"),this.storage.get("show_hidden"),this.filters);this.$datasetsList(n).empty();if(o.length){o.each(function(r){var q=r.id,s=m._createContentView(r);p[q]=s;if(_.contains(m.selectedHdaIds,q)){s.selected=true}m.attachContentView(s.render(),n)})}this.hdaViews=p;this._renderEmptyMsg(n);return this.hdaViews},_createContentView:function(p){var n=this._getContentClass(p),m=_.extend(this._getContentOptions(p),{model:p}),o=new n(m);this._setUpHdaListeners(o);return o},_getContentClass:function(m){var n=m.get("history_content_type");switch(n){case"dataset":return this.HDAViewClass;case"dataset_collection":return this.HDCAViewClass}throw new TypeError("Unknown history_content_type: "+n)},_getContentOptions:function(m){return{linkTarget:this.linkTarget,expanded:!!this.storage.get("expandedHdas")[m.id],hasUser:this.model.ownedByCurrUser(),logger:this.logger}},_setUpHdaListeners:function(n){var m=this;n.on("error",function(p,r,o,q){m.errorHandler(p,r,o,q)});n.on("expanded",function(o){m.storage.addExpandedHda(o.model)});n.on("collapsed",function(o){m.storage.removeExpandedHda(o.model)});return this},attachContentView:function(o,n){n=n||this.$el;var m=this.$datasetsList(n);m.prepend(o.$el);return this},addContentView:function(p){this.log("add."+this,p);var n=this;if(!p.isVisible(this.storage.get("show_deleted"),this.storage.get("show_hidden"))){return n}$({}).queue([function o(r){var q=n.$emptyMessage();if(q.is(":visible")){q.fadeOut(n.fxSpeed,r)}else{r()}},function m(q){var r=n._createContentView(p);n.hdaViews[p.id]=r;r.render().$el.hide();n.scrollToTop();n.attachContentView(r);r.$el.slideDown(n.fxSpeed)}]);return n},views:function(m){var n=this,o=this.model.contents.getVisible(this.storage.get("show_deleted"),this.storage.get("show_hidden"),this.filters);if(m!==undefined){return n.hdaViews[o.at(m).id]}return o.map(function(p){return n.hdaViews[p.id]})},refreshContents:function(n,m){if(this.model){return this.model.refresh(n,m)}return $.when()},hdaViewRange:function(p,o){if(p===o){return[p]}var m=this,n=false,q=[];this.model.contents.getVisible(this.storage.get("show_deleted"),this.storage.get("show_hidden"),this.filters).each(function(r){if(n){q.push(m.hdaViews[r.id]);if(r===p.model||r===o.model){n=false}}else{if(r===p.model||r===o.model){n=true;q.push(m.hdaViews[r.id])}}});return q},events:{"click .message-container":"clearMessages","click .history-search-btn":"toggleSearchControls"},collapseAllHdaBodies:function(){_.each(this.hdaViews,function(m){m.collapse()});this.storage.set("expandedHdas",{});return this},toggleShowDeleted:function(m){m=(m!==undefined)?(m):(!this.storage.get("show_deleted"));this.storage.set("show_deleted",m);this.renderHdas();return this.storage.get("show_deleted")},toggleShowHidden:function(m){m=(m!==undefined)?(m):(!this.storage.get("show_hidden"));this.storage.set("show_hidden",m);this.renderHdas();return this.storage.get("show_hidden")},_setUpSearchInput:function(n){var o=this,p=".history-search-input";function m(q){if(o.model.contents.haveDetails()){o.searchHdas(q);return}o.$el.find(p).searchInput("toggle-loading");o.model.contents.fetchAllDetails({silent:true}).always(function(){o.$el.find(p).searchInput("toggle-loading")}).done(function(){o.searchHdas(q)})}n.searchInput({initialVal:o.searchFor,name:"history-search",placeholder:"search datasets",classes:"history-search",onfirstsearch:m,onsearch:_.bind(this.searchHdas,this),onclear:_.bind(this.clearHdaSearch,this)});return n},toggleSearchControls:function(o,m){var n=this.$el.find(".history-search-controls"),p=(jQuery.type(o)==="number")?(o):(this.fxSpeed);m=(m!==undefined)?(m):(!n.is(":visible"));if(m){n.slideDown(p,function(){$(this).find("input").focus()})}else{n.slideUp(p)}return m},searchHdas:function(m){var n=this;this.searchFor=m;this.filters=[function(o){return o.matchesAll(n.searchFor)}];this.trigger("search:searching",m,this);this.renderHdas();return this},clearHdaSearch:function(m){this.searchFor="";this.filters=[];this.trigger("search:clear",this);this.renderHdas();return this},_showLoadingIndicator:function(n,m,o){m=(m!==undefined)?(m):(this.fxSpeed);if(!this.indicator){this.indicator=new LoadingIndicator(this.$el,this.$el.parent())}if(!this.$el.is(":visible")){this.indicator.show(0,o)}else{this.$el.fadeOut(m);this.indicator.show(n,m,o)}},_hideLoadingIndicator:function(m,n){m=(m!==undefined)?(m):(this.fxSpeed);if(this.indicator){this.indicator.hide(m,n)}},displayMessage:function(r,s,q){var o=this;this.scrollToTop();var p=this.$messages(),m=$("").addClass(r+"message").html(s);if(!_.isEmpty(q)){var n=$('Details').click(function(){Galaxy.modal.show(o._messageToModalOptions(r,s,q));return false});m.append(" ",n)}return p.html(m)},_messageToModalOptions:function(q,s,p){var m=this,r=$(""),o={title:"Details"};function n(t){t=_.omit(t,_.functions(t));return["
"]);return _.extend(_.clone(e.prototype.templates),{controls:f})}());return{ListPanel:e,ModelListPanel:a}});
\ No newline at end of file
diff --git a/static/scripts/packed/mvc/tools/tools-form.js b/static/scripts/packed/mvc/tools/tools-form.js
index 1ea9961ac41..5308c575108 100644
--- a/static/scripts/packed/mvc/tools/tools-form.js
+++ b/static/scripts/packed/mvc/tools/tools-form.js
@@ -1 +1 @@
-define(["mvc/ui/ui-portlet","mvc/ui/ui-misc","mvc/citation/citation-model","mvc/citation/citation-view","mvc/tools","mvc/tools/tools-template","mvc/tools/tools-datasets","mvc/tools/tools-section","mvc/tools/tools-tree"],function(g,k,i,a,f,d,h,j,c){var e=Backbone.Model.extend({initialize:function(l){this.url=galaxy_config.root+"api/tools/"+l.id+"?io_details=true"}});var b=Backbone.View.extend({main_el:"body",initialize:function(m){var l=this;this.options=m;this.model=new e({id:m.id});this.tree=new c(this);this.field_list={};this.input_list={};this.datasets=new h({history_id:this.options.history_id,success:function(){l._initializeToolForm()}})},_initializeToolForm:function(){var m=this;var n=new k.ButtonIcon({icon:"fa-question-circle",title:"Question?",tooltip:"Ask a question about this tool (Biostar)",onclick:function(){window.open(m.options.biostar_url+"/p/new/post/")}});var o=new k.ButtonIcon({icon:"fa-search",title:"Search",tooltip:"Search help for this tool (Biostar)",onclick:function(){window.open(m.options.biostar_url+"/t/"+m.options.id+"/")}});var l=new k.ButtonIcon({icon:"fa-share",title:"Share",tooltip:"Share this tool",onclick:function(){prompt("Copy to clipboard: Ctrl+C, Enter",galaxy_config.root+"root?tool_id="+m.options.id)}});this.model.fetch({error:function(p){console.debug("tools-form::_initializeToolForm() : Attempt to fetch tool model failed.")},success:function(){m.inputs=m.model.get("inputs");m.portlet=new g.View({icon:"fa-wrench",title:""+m.model.get("name")+" "+m.model.get("description"),buttons:{execute:new k.ButtonIcon({icon:"fa-check",tooltip:"Execute the tool",title:"Execute",floating:"clear",onclick:function(){m._submit()}})},operations:{button_question:n,button_search:o,button_share:l}});if(!m.options.biostar_url){n.$el.hide();o.$el.hide()}m.message=new k.Message();m.portlet.append(m.message.$el);$(m.main_el).append(m.portlet.$el);if(m.options.help!=""){$(m.main_el).append(d.help(m.options.help))}if(m.options.citations){$(m.main_el).append(d.citations());var p=new i.ToolCitationCollection();p.tool_id=m.options.id;var q=new a.CitationListView({collection:p});q.render();p.fetch()}m.setElement(m.portlet.content());m.section=new j.View(m,{inputs:m.model.get("inputs")});m.portlet.append(m.section.$el);m.refresh()}})},refresh:function(){this.tree.refresh();for(var l in this.field_list){this.field_list[l].trigger("change")}console.debug("tools-form::refresh() - Recreated tree structure. Refresh.")},_submit:function(){console.log(this.tree.jobDictionary())}});return{View:b}});
\ No newline at end of file
+define(["mvc/ui/ui-portlet","mvc/ui/ui-misc","mvc/citation/citation-model","mvc/citation/citation-view","mvc/tools","mvc/tools/tools-template","mvc/tools/tools-datasets","mvc/tools/tools-section","mvc/tools/tools-tree"],function(g,k,i,a,f,d,h,j,c){var e=Backbone.Model.extend({initialize:function(l){this.url=galaxy_config.root+"api/tools/"+l.id+"?io_details=true"}});var b=Backbone.View.extend({main_el:"body",initialize:function(m){var l=this;this.options=m;this.model=new e({id:m.id});this.tree=new c(this);this.field_list={};this.input_list={};this.datasets=new h({history_id:this.options.history_id,success:function(){l._initializeToolForm()}})},_initializeToolForm:function(){var m=this;var n=new k.ButtonIcon({icon:"fa-question-circle",title:"Question?",tooltip:"Ask a question about this tool (Biostar)",onclick:function(){window.open(m.options.biostar_url+"/p/new/post/")}});var o=new k.ButtonIcon({icon:"fa-search",title:"Search",tooltip:"Search help for this tool (Biostar)",onclick:function(){window.open(m.options.biostar_url+"/t/"+m.options.id+"/")}});var l=new k.ButtonIcon({icon:"fa-share",title:"Share",tooltip:"Share this tool",onclick:function(){prompt("Copy to clipboard: Ctrl+C, Enter",galaxy_config.root+"root?tool_id="+m.options.id)}});this.model.fetch({error:function(p){console.debug("tools-form::_initializeToolForm() : Attempt to fetch tool model failed.")},success:function(){m.inputs=m.model.get("inputs");m.portlet=new g.View({icon:"fa-wrench",title:""+m.model.get("name")+" "+m.model.get("description"),buttons:{execute:new k.ButtonIcon({icon:"fa-check",tooltip:"Execute the tool",title:"Execute",floating:"clear",onclick:function(){m._submit()}})},operations:{button_question:n,button_search:o,button_share:l}});if(!m.options.biostar_url){n.$el.hide();o.$el.hide()}m.message=new k.Message();m.portlet.append(m.message.$el);$(m.main_el).append(m.portlet.$el);if(m.options.help!=""){$(m.main_el).append(d.help(m.options.help))}if(m.options.citations){$(m.main_el).append(d.citations());var p=new i.ToolCitationCollection();p.tool_id=m.options.id;var q=new a.CitationListView({collection:p});q.render();p.fetch()}m.setElement(m.portlet.content());m.section=new j.View(m,{inputs:m.model.get("inputs")});m.portlet.append(m.section.$el);m.refresh();m._submit()}})},refresh:function(){this.tree.refresh();for(var l in this.field_list){this.field_list[l].trigger("change")}console.debug("tools-form::refresh() - Recreated tree structure. Refresh.")},_submit:function(){console.log(this.tree.finalize())}});return{View:b}});
\ No newline at end of file
diff --git a/static/scripts/packed/mvc/tools/tools-tree.js b/static/scripts/packed/mvc/tools/tools-tree.js
index dbed81ff07d..0eb9021032a 100644
--- a/static/scripts/packed/mvc/tools/tools-tree.js
+++ b/static/scripts/packed/mvc/tools/tools-tree.js
@@ -1 +1 @@
-define([],function(){return Backbone.Model.extend({initialize:function(a){this.app=a},refresh:function(){if(!this.app.section){return{}}this.dict={};this.xml=$("");this._iterate(this.app.section.$el,this.dict,this.xml)},jobDictionary:function(){var a=this;var b={};function c(e,g){for(var f in g){if(g[f].input){var d=g[f].input;var h=e+d.name;if(d.type=="repeat"){}else{switch(d.type){case"repeat":b[h]=value;break;default:b[h]=value}}}}}c("",this.dict);return b},_iterate:function(d,e,b){var a=this;var c=$(d).children();c.each(function(){var i=this;var h=$(i).attr("id");if($(i).hasClass("section-row")||$(i).hasClass("tab-pane")){e[h]={};var f=a.app.input_list[h];if(f){e[h]={input:f}}var g=$('');b.append(g);a._iterate(i,e[h],g)}else{a._iterate(i,e,b)}})},findReferences:function(c,e){var g=[];var b=this;function d(h,j){var i=$(j).children();var l=[];var k=false;i.each(function(){var o=this;var n=$(o).attr("id");if(n!==c){var m=b.app.input_list[n];if(m){if(m.name==h){k=true;return false}if(m.data_ref==h&&m.type==e){l.push(n)}}}});if(!k){g=g.concat(l);i.each(function(){d(h,this)})}}var f=this.xml.find("#"+c);if(f.length>0){var a=this.app.input_list[c];if(a){d(a.name,f.parent())}}return g}})});
\ No newline at end of file
+define([],function(){return Backbone.Model.extend({initialize:function(a){this.app=a},refresh:function(){this.dict={};this.xml=$("");if(!this.app.section){return{}}this._iterate(this.app.section.$el,this.dict,this.xml)},finalize:function(){var a=this;var b={};function c(k,l){for(var f in l){var d=l[f];if(d.input){var m=d.input;var h=k;if(k!=""){h+="|"}h+=m.name;switch(m.type){case"repeat":var g=0;for(var e in d){if(e.indexOf("section")!=-1){c(h+"_"+g++,d[e])}}break;case"conditional":var n=a.app.field_list[m.id].value();for(var e in m.cases){if(m.cases[e].value==n){c(h,l[m.id+"-section-"+e]);break}}break;default:var n=a.app.field_list[m.id].value();b[h]=n}}}}c("",this.dict);return b},findReferences:function(c,e){var g=[];var b=this;function d(h,j){var i=$(j).children();var l=[];var k=false;i.each(function(){var o=this;var n=$(o).attr("id");if(n!==c){var m=b.app.input_list[n];if(m){if(m.name==h){k=true;return false}if(m.data_ref==h&&m.type==e){l.push(n)}}}});if(!k){g=g.concat(l);i.each(function(){d(h,this)})}}var f=this.xml.find("#"+c);if(f.length>0){var a=this.app.input_list[c];if(a){d(a.name,f.parent())}}return g},_iterate:function(d,e,b){var a=this;var c=$(d).children();c.each(function(){var i=this;var h=$(i).attr("id");if($(i).hasClass("section-row")||$(i).hasClass("tab-pane")){e[h]={};var f=a.app.input_list[h];if(f){e[h]={input:f}}var g=$('');b.append(g);a._iterate(i,e[h],g)}else{a._iterate(i,e,b)}})}})});
\ No newline at end of file
diff --git a/static/scripts/packed/mvc/ui.js b/static/scripts/packed/mvc/ui.js
index 433aa09bbb4..60fcf202d27 100644
--- a/static/scripts/packed/mvc/ui.js
+++ b/static/scripts/packed/mvc/ui.js
@@ -1 +1 @@
-var IconButton=Backbone.Model.extend({defaults:{title:"",icon_class:"",on_click:null,menu_options:null,is_menu_button:true,id:null,href:null,target:null,enabled:true,visible:true,tooltip_config:{}}});var IconButtonView=Backbone.View.extend({initialize:function(){this.model.attributes.tooltip_config={placement:"bottom"};this.model.bind("change",this.render,this)},render:function(){this.$el.tooltip("hide");var a=this.template(this.model.toJSON());a.tooltip(this.model.get("tooltip_config"));this.$el.replaceWith(a);this.setElement(a);return this},events:{click:"click"},click:function(a){if(_.isFunction(this.model.get("on_click"))){this.model.get("on_click")(a);return false}return true},template:function(b){var a='title="'+b.title+'" class="icon-button';if(b.is_menu_button){a+=" menu-button"}a+=" "+b.icon_class;if(!b.enabled){a+="_disabled"}a+='"';if(b.id){a+=' id="'+b.id+'"'}a+=' href="'+b.href+'"';if(b.target){a+=' target="'+b.target+'"'}if(!b.visible){a+=' style="display: none;"'}if(b.enabled){a=""}else{a=""}return $(a)}});var IconButtonCollection=Backbone.Collection.extend({model:IconButton});var IconButtonMenuView=Backbone.View.extend({tagName:"div",initialize:function(){this.render()},render:function(){var a=this;this.collection.each(function(d){var b=$("").attr("href","javascript:void(0)").attr("title",d.attributes.title).addClass("icon-button menu-button").addClass(d.attributes.icon_class).appendTo(a.$el).click(d.attributes.on_click);if(d.attributes.tooltip_config){b.tooltip(d.attributes.tooltip_config)}var c=d.get("options");if(c){make_popupmenu(b,c)}});return this}});var create_icon_buttons_menu=function(b,a){if(!a){a={}}var c=new IconButtonCollection(_.map(b,function(d){return new IconButton(_.extend(d,a))}));return new IconButtonMenuView({collection:c})};var Grid=Backbone.Collection.extend({});var GridView=Backbone.View.extend({});var PopupMenu=Backbone.View.extend({initialize:function(b,a){this.$button=b;if(!this.$button.size()){this.$button=$("")}this.options=a||[];var c=this;this.$button.click(function(d){$(".popmenu-wrapper").remove();c._renderAndShow(d);return false})},_renderAndShow:function(a){this.render();this.$el.appendTo("body").css(this._getShownPosition(a)).show();this._setUpCloseBehavior()},render:function(){this.$el.addClass("popmenu-wrapper").hide().css({position:"absolute"}).html(this.template(this.$button.attr("id"),this.options));if(this.options.length){var a=this;this.$el.find("li").each(function(c,b){var d=a.options[c];if(d.func){$(this).children("a.popupmenu-option").click(function(e){d.func.call(a,e,d)})}})}return this},template:function(b,a){return['
").addClass("pagination pagination-page-list"),r=function(t){n.goToPage($(this).data("page"))};for(var o=0;o=this.numPages){n=this.numPages-1}if(n===this.currPage){return this}this.currPage=n;this.$element.trigger("pagination.page-change",this.currPage);this._render();return this};i.prototype.prevPage=function c(){return this.goToPage(this.currPage-1)};i.prototype.nextPage=function h(){return this.goToPage(this.currPage+1)};i.prototype.page=function f(){return this.currPage};i.create=function k(n,o){return new i(n,o)};jQuery.fn.extend({pagination:function d(o){var n=jQuery.makeArray(arguments).slice(1);if(jQuery.type(o)==="object"){return this.map(function(){i.create($(this),o);return this})}var q=$(this[0]),r=q.data(i.prototype.DATA_KEY);if(r){if(jQuery.type(o)==="string"){var p=r[o];if(jQuery.type(p)==="function"){return p.apply(r,n)}}else{return r}}return undefined}})}());(function(){var g={renameColumns:false,columnNames:[],commentChar:"#",hideCommentRows:false,includePrompts:true,topLeftContent:"Columns:"},s="peek-control.change",t="peek-control.rename",l="peek-control",v="control",f="control-prompt",c="selected",n="disabled",u="button",q="renamable-header",p="column-index",a="column-name";function i(w){if(w.disabled&&jQuery.type(w.disabled)!=="array"){throw new Error('"disabled" must be defined as an array of indeces: '+JSON.stringify(w))}if(w.multiselect&&w.selected&&jQuery.type(w.selected)!=="array"){throw new Error('Mulitselect rows need an array for "selected": '+JSON.stringify(w))}if(!w.label||!w.id){throw new Error("Peek controls need a label and id for each control row: "+JSON.stringify(w))}if(w.disabled&&w.disabled.indexOf(w.selected)!==-1){throw new Error("Selected column is in the list of disabled columns: "+JSON.stringify(w))}return w}function o(x,w){return $("").addClass(u).text(x.label)}function k(x,w){var y=$("
").addClass("pagination pagination-page-list"),r=function(t){n.goToPage($(this).data("page"))};for(var o=0;o=this.numPages){n=this.numPages-1}if(n===this.currPage){return this}this.currPage=n;this.$element.trigger("pagination.page-change",this.currPage);this._render();return this};i.prototype.prevPage=function c(){return this.goToPage(this.currPage-1)};i.prototype.nextPage=function h(){return this.goToPage(this.currPage+1)};i.prototype.page=function f(){return this.currPage};i.create=function k(n,o){return new i(n,o)};jQuery.fn.extend({pagination:function d(o){var n=jQuery.makeArray(arguments).slice(1);if(jQuery.type(o)==="object"){return this.map(function(){i.create($(this),o);return this})}var q=$(this[0]),r=q.data(i.prototype.DATA_KEY);if(r){if(jQuery.type(o)==="string"){var p=r[o];if(jQuery.type(p)==="function"){return p.apply(r,n)}}else{return r}}return undefined}})}());(function(){var g={renameColumns:false,columnNames:[],commentChar:"#",hideCommentRows:false,includePrompts:true,topLeftContent:"Columns:"},s="peek-control.change",t="peek-control.rename",l="peek-control",v="control",f="control-prompt",c="selected",n="disabled",u="button",q="renamable-header",p="column-index",a="column-name";function i(w){if(w.disabled&&jQuery.type(w.disabled)!=="array"){throw new Error('"disabled" must be defined as an array of indeces: '+JSON.stringify(w))}if(w.multiselect&&w.selected&&jQuery.type(w.selected)!=="array"){throw new Error('Mulitselect rows need an array for "selected": '+JSON.stringify(w))}if(!w.label||!w.id){throw new Error("Peek controls need a label and id for each control row: "+JSON.stringify(w))}if(w.disabled&&w.disabled.indexOf(w.selected)!==-1){throw new Error("Selected column is in the list of disabled columns: "+JSON.stringify(w))}return w}function o(x,w){return $("").addClass(u).text(x.label)}function k(x,w){var y=$("
";return c},_template_tab_content:function(c){return''}});return{View:b}});
\ No newline at end of file
+define(["utils/utils"],function(a){var b=Backbone.View.extend({optionsDefault:{title_new:"",operations:null,onnew:null,min:null,max:null,onchange:null},initialize:function(e){this.visible=false;this.$nav=null;this.$content=null;this.first_tab=null;this.current_id=null;this.options=a.merge(e,this.optionsDefault);var c=$(this._template(this.options));this.$nav=c.find(".tab-navigation");this.$content=c.find(".tab-content");this.setElement(c);this.list={};var d=this;if(this.options.operations){$.each(this.options.operations,function(g,h){h.$el.prop("id",g);d.$nav.find(".operations").append(h.$el)})}if(this.options.onnew){var f=$(this._template_tab_new(this.options));this.$nav.append(f);f.tooltip({title:"Add a new tab",placement:"bottom",container:d.$el});f.on("click",function(g){f.tooltip("hide");d.options.onnew()})}},size:function(){return _.size(this.list)},current:function(){return this.$el.find(".tab-pane.active").attr("id")},add:function(f){var e=this;var h=f.id;var g=$(this._template_tab(f));var d=$(this._template_tab_content(f));this.list[h]=f.ondel?true:false;if(this.options.onnew){this.$nav.find("#new-tab").before(g)}else{this.$nav.append(g)}d.append(f.$el);this.$content.append(d);if(this.size()==1){g.addClass("active");d.addClass("active");this.first_tab=h}if(this.options.max&&this.size()>=this.options.max){this.$el.find("#new-tab").hide()}if(f.ondel){var c=g.find("#delete");c.tooltip({title:"Delete this tab",placement:"bottom",container:e.$el});c.on("click",function(){c.tooltip("destroy");e.$el.find(".tooltip").remove();f.ondel();return false})}g.on("click",function(i){i.preventDefault();if(f.onclick){f.onclick()}else{e.show(h)}});if(!this.current_id){this.current_id=h}},del:function(c){this.$el.find("#tab-"+c).remove();this.$el.find("#"+c).remove();if(this.first_tab==c){this.first_tab=null}if(this.first_tab!=null){this.show(this.first_tab)}if(this.list[c]){delete this.list[c]}if(this.size()