commit/galaxy-central: carlfeberhard: history.js: add persistant storage, show prev. opened datasets on page refresh; base-mvc.js: add PersistantStorage object adapter
1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/changeset/724aaf15bcbe/ changeset: 724aaf15bcbe user: carlfeberhard date: 2012-10-08 19:27:11 summary: history.js: add persistant storage, show prev. opened datasets on page refresh; base-mvc.js: add PersistantStorage object adapter affected #: 3 files diff -r c456d67423b6988de9f9777d9758901845d02de8 -r 724aaf15bcbe4989450af00c83ea8538c6b2f051 static/scripts/mvc/base-mvc.js --- a/static/scripts/mvc/base-mvc.js +++ b/static/scripts/mvc/base-mvc.js @@ -41,6 +41,7 @@ } }); + //============================================================================== /** * Adds logging capabilities to your Models/Views @@ -69,6 +70,7 @@ } }; + // ============================================================================= /** Global string localization object (and global short form alias) * set with either: @@ -78,8 +80,8 @@ * _l( original ) */ //TODO: move to Galaxy.Localization (maybe galaxy.base.js) -var GalaxyLocalization = jQuery.extend({}, { - aliasName : '_l', +var GalaxyLocalization = jQuery.extend( {}, { + ALIAS_NAME : '_l', localizedStrings : {}, setLocalizedString : function( str_or_obj, localizedString ){ @@ -127,7 +129,7 @@ }); // global localization alias -window[ GalaxyLocalization.aliasName ] = function( str ){ return GalaxyLocalization.localize( str ); }; +window[ GalaxyLocalization.ALIAS_NAME ] = function( str ){ return GalaxyLocalization.localize( str ); }; //TEST: setLocalizedString( string, string ), _l( string ) //TEST: setLocalizedString( hash ), _l( string ) @@ -135,203 +137,112 @@ //TEST: _l( non assigned string ) - //============================================================================== /** - * Base class for template loaders: - * The main interface is loader.getTemplates( templatesToLoad ) - * where templatesToLoad is in the form: - * { - * remoteTemplateFilename1: { - * templateFunctionName1 : templateID1, - * templateFunctionName2 : templateID2, - * ... - * }, - * remoteTemplateFilename2: { - * templateFunctionName3 : templateID3, - * templateFunctionName4 : templateID4, - * ... - * } - * } - * getTemplates will return a map of the templates in the form: - * { - * templateFunctionName1 : compiledTemplateFn1(), - * templateFunctionName2 : compiledTemplateFn2(), - * templateFunctionName3 : compiledTemplateFn3(), - * ... - * } + * @class PersistantStorage + * persistant storage adapter to: + * provide an easy interface to object based storage using method chaining + * allow easy change of the storage engine used (h5's local storage?) * - * Generally meant to be called for Backbone views, etc like this: - * BackboneView.templates = CompiledTemplateLoader( templatesToLoad ); + * @param {String} storageKey : the key the storage engine will place the storage object under + * @param {Object} storageDefaults : [optional] initial object to set up storage with + * + * @example : + * HistoryPanel.storage = new PersistanStorage( HistoryPanel.toString(), { visibleItems, {} }) + * itemView.bind( 'toggleBodyVisibility', function( id, visible ){ + * if( visible ){ + * HistoryPanel.storage.get( 'visibleItems' ).set( id, true ); + * } else { + * HistoryPanel.storage.get( 'visibleItems' ).deleteKey( id ); + * } + * }); */ -var TemplateLoader = _.extend( {}, LoggableMixin, { - //TODO: incorporate caching of template functions (for use across objects) - //TODO: only require and use 2 level (or some variation) map templatesToLoad for the remote loader - - // comment next line out to suppress logging - //logger : console, - - //cachedTemplates : {}, - - getTemplateLoadFn : function(){ - throw( "There is no templateLoadFn. Make sure you're using a subclass of TemplateLoader" ); - }, - - // loop through templatesToLoad assuming it is a map in the form mentioned above - getTemplates : function( templatesToLoad, forceReload ){ - forceReload = forceReload || false; - this.log( this, 'getTemplates:', templatesToLoad, ', forceReload:', forceReload ); - - //!TODO: cache templates here - var templates = {}, - loader = this, - templateLoadFn = this.getTemplateLoadFn(); - - if( !templatesToLoad ){ return templates; } - jQuery.each( templatesToLoad, function( templateFile, templateData ){ - - //TODO: handle flatter map versions of templatesToLoad ({ name : id }) - jQuery.each( templateData, function( templateName, templateID ){ - loader.log( loader + ', templateFile:', templateFile, - 'templateName:', templateName, ', templateID:', templateID ); - templates[ templateName ] = templateLoadFn.call( loader, templateFile, templateName, templateID ); - }); - }); - return templates; +var PersistantStorage = function( storageKey, storageDefaults ){ + if( !storageKey ){ + throw( "PersistantStorage needs storageKey argument" ); } -}); + storageDefaults = storageDefaults || {}; + // ~constants for the current engine + //TODO:?? this would be greatly simplified if we're IE9+ only (setters/getters) + var STORAGE_ENGINE_GETTER = jQuery.jStorage.get, + STORAGE_ENGINE_SETTER = jQuery.jStorage.set, + STORAGE_ENGINE_KEY_DELETER = jQuery.jStorage.deleteKey; -//.............................................................................. -/** find the compiled template in Handlebars.templates by templateName - * and return the entire, requested templates map - */ -var CompiledTemplateLoader = _.extend( {}, TemplateLoader, { - getTemplateLoadFn : function(){ return this.loadCompiledHandlebarsTemplate; }, - - // override if new compiler - loadCompiledHandlebarsTemplate : function( templateFile, templateName, templateID ){ - //pre: compiled templates should have been loaded with the mako helper h.templates - // (although these could be dynamically loaded as well?) - this.log( 'getInDomTemplates, templateFile:', templateFile, - 'templateName:', templateName, ', templateID:', templateID ); - - if( !Handlebars.templates || !Handlebars.templates[ templateID ] ){ - throw( 'Template not found: Handlebars.' + templateID - + '. Check your h.templates() call in the mako file that rendered this page' ); - } - this.log( 'found template function:', templateID ); - // really this is just a lookup - return Handlebars.templates[ templateID ]; - } - - //TEST: Handlebars.full NOT runtime - //TEST: no Handlebars - //TEST: bad id - //TEST: Handlebars.runtime, good id -}); + // recursion helper for method chaining access + var StorageRecursionHelper = function( data, parent ){ + //console.debug( 'new StorageRecursionHelper. data:', data ); + data = data || {}; + parent = parent || null; + return { + // get a value from the storage obj named 'key', + // if it's an object - return a new StorageRecursionHelper wrapped around it + // if it's something simpler - return the value + // if this isn't passed a key - return the data at this level of recursion + get : function( key ){ + //console.debug( this + '.get', key ); + if( key === undefined ){ + return data; + } else if( data.hasOwnProperty( key ) ){ + return ( jQuery.type( data[ key ] ) === 'object' )? + ( new StorageRecursionHelper( data[ key ], this ) ) + :( data[ key ] ); + } + return undefined; + }, + // set a value on the current data - then pass up to top to save current entire object in storage + set : function( key, value ){ + //TODO: add parameterless variation setting the data somehow + // ??: difficult bc of obj by ref, closure + //console.debug( this + '.set', key, value ); + data[ key ] = value; + this.save(); + return this; + }, + // remove a key at this level - then save entire (as 'set' above) + deleteKey : function( key ){ + //console.debug( this + '.deleteKey', key ); + delete data[ key ]; + this.save(); + return this; + }, + // pass up the recursion chain (see below for base case) + save : function(){ + //console.debug( this + '.save', parent ); + return parent.save(); + }, + toString : function(){ + return ( 'StorageRecursionHelper(' + data + ')' ); + } + }; + }; -//.............................................................................. -/** find the NON-compiled template templateID in the DOM, compile it (using Handlebars), - * and return the entire, requested templates map - * (Note: for use with Mako.include and multiple templates) - */ -var InDomTemplateLoader = _.extend( {}, TemplateLoader, { - - // override or change if a new compiler (Underscore, etc.) is being used - compileTemplate : function( templateText ){ - // we'll need the compiler - if( !Handlebars || !Handlebars.compile ){ - throw( 'No Handlebars.compile found. You may only have Handlebars.runtime loaded.' - + 'Include handlebars.full for this to work' ); - } - // copy fn ref to this view under the templateName - this.log( 'compiling template:', templateText ); - return Handlebars.compile( templateText ); - }, - - findTemplateInDom : function( templateFile, templateName, templateID ){ - // assume the last is best - return $( 'script#' + templateID ).last(); - }, - - getTemplateLoadFn : function(){ return this.loadInDomTemplate; }, - - loadInDomTemplate : function( templateFile, templateName, templateID ){ - this.log( 'getInDomTemplate, templateFile:', templateFile, - 'templateName:', templateName, ', templateID:', templateID ); - - // find it in the dom by the id and compile - var template = this.findTemplateInDom( templateFile, templateName, templateID ); - if( !template || !template.length ){ - throw( 'Template not found within the DOM: ' + templateID - + '. Check that this template has been included in the page' ); - } - this.log( 'found template in dom:', template.html() ); - return this.compileTemplate( template.html() ); + //??: more readable to make another class? + var returnedStorage = {}; + // attempt to get starting data from engine... + data = STORAGE_ENGINE_GETTER( storageKey ); + + // ...if that fails, use the defaults (and store them) + if( data === null ){ + //console.debug( 'no previous data. using defaults...' ); + data = jQuery.extend( true, {}, storageDefaults ); + STORAGE_ENGINE_SETTER( storageKey, data ); } - //TEST: no compiler - //TEST: good url, good id, in DOM - //TEST: good url, good id, NOT in DOM -}); + // the object returned by this constructor will be a modified StorageRecursionHelper + returnedStorage = new StorageRecursionHelper( data ); + // the base case for save()'s upward recursion - save everything to storage + returnedStorage.save = function( newData ){ + //console.debug( returnedStorage, '.save:', JSON.stringify( returnedStorage.get() ) ); + STORAGE_ENGINE_SETTER( storageKey, returnedStorage.get() ); + }; + // delete function to remove the base data object from the storageEngine + returnedStorage.destroy = function(){ + //console.debug( returnedStorage, '.destroy:' ); + STORAGE_ENGINE_KEY_DELETER( storageKey ); + }; + returnedStorage.toString = function(){ return 'PersistantStorage(' + data + ')'; }; + + return returnedStorage; +}; -//.............................................................................. -/** HTTP GET the NON-compiled templates, append into the DOM, compile them, - * and return the entire, requested templates map - * (for use with dynamically loaded views) - */ -var RemoteTemplateLoader = _.extend( {}, InDomTemplateLoader, { - templateBaseURL : 'static/scripts/templates/', - - getTemplateLoadFn : function(){ return this.loadViaHttpGet; }, - - loadViaHttpGet : function( templateFile, templateName, templateID ){ - var templateBaseURL = 'static/scripts/templates/'; - this.log( 'loadViaHttpGet, templateFile:', templateFile, - 'templateName:', templateName, ', templateID:', templateID, - 'templateBaseURL:', this.templateBaseURL ); - - //??: possibly not the best pattern here... - // try in-dom first (prevent loading the same templateFile for each of its templates) - var template = null; - try { - template = this.loadInDomTemplate( templateFile, templateName, templateID ); - - // if that didn't work, load the templateFile via GET,... - } catch( exception ){ - this.log( 'getInDomTemplate exception:' + exception ); - // handle no compiler exception - if( !Handlebars.compile ){ throw( exception ); } - //TEST: - - this.log( "Couldn't locate template in DOM: " + templateID ); - var loader = this; - var url = templateBaseURL + templateFile; - //??: async : false may cause problems in the long run - jQuery.ajax( url, { - method : 'GET', - async : false, - success : function( data ){ - loader.log( templateFile + ' loaded via GET. Attempting compile...' ); - //...move the templateFile into the DOM and try that again - $( 'body' ).append( data ); - template = loader.loadInDomTemplate( templateFile, templateName, templateID ); - }, - error : function( data, status, xhr ){ - throw( 'Failed to fetch ' + url + ':' + status ); - } - }); - } - if( !template ){ - throw( "Couldn't load or fetch template: " + templateID ); - } - return template; - } - - //TEST: no compiler - //TEST: good url, good id, already local - //TEST: good url, good id, remote load - //TEST: good url, bad template id - //TEST: bad url, error from ajax -}); diff -r c456d67423b6988de9f9777d9758901845d02de8 -r 724aaf15bcbe4989450af00c83ea8538c6b2f051 static/scripts/mvc/history.js --- a/static/scripts/mvc/history.js +++ b/static/scripts/mvc/history.js @@ -6,6 +6,8 @@ Backbone.js implementation of history panel TODO: + currently, adding a dataset (via tool execute, etc.) creates a new dataset and refreshes the page + meta: require.js convert function comments to jsDoc style, complete comments @@ -142,8 +144,9 @@ className : "historyItemContainer", // ................................................................................ SET UP - initialize : function(){ + initialize : function( attributes ){ this.log( this + '.initialize:', this, this.model ); + this.visible = attributes.visible; }, // ................................................................................ RENDER MAIN @@ -209,6 +212,7 @@ return buttonDiv; }, + //TODO: ?? the three title buttons render for err'd datasets: is this normal? _render_displayButton : function(){ // don't show display while uploading if( this.model.get( 'state' ) === HistoryItem.STATES.UPLOAD ){ return null; } @@ -608,6 +612,11 @@ if( this.model.get( 'bodyIsShown' ) === false ){ body.hide(); } + if( this.visible ){ + body.show(); + } else { + body.hide(); + } return body; }, @@ -693,14 +702,15 @@ return false; }, - toggleBodyVisibility : function(){ - this.log( this + '.toggleBodyVisibility' ); - this.$el.find( '.historyItemBody' ).toggle(); + toggleBodyVisibility : function( visible ){ + var $body = this.$el.find( '.historyItemBody' ); + $body.toggle(); + this.trigger( 'toggleBodyVisibility', this.model.get( 'id' ), $body.is( ':visible' ) ); }, // ................................................................................ UTILTIY toString : function(){ - var modelString = ( this.model )?( this.model + '' ):( '' ); + var modelString = ( this.model )?( this.model + '' ):( '(no model)' ); return 'HistoryItemView(' + modelString + ')'; } }); @@ -708,21 +718,18 @@ //------------------------------------------------------------------------------ //HistoryItemView.templates = InDomTemplateLoader.getTemplates({ -HistoryItemView.templates = CompiledTemplateLoader.getTemplates({ - 'common-templates.html' : { - warningMsg : 'template-warningmessagesmall' - }, - 'history-templates.html' : { - messages : 'template-history-warning-messages', - titleLink : 'template-history-titleLink', - hdaSummary : 'template-history-hdaSummary', - downloadLinks : 'template-history-downloadLinks', - failedMetadata : 'template-history-failedMetaData', - tagArea : 'template-history-tagArea', - annotationArea : 'template-history-annotationArea', - displayApps : 'template-history-displayApps' - } -}); +HistoryItemView.templates = { + warningMsg : Handlebars.templates[ 'template-warningmessagesmall' ], + + messages : Handlebars.templates[ 'template-history-warning-messages' ], + titleLink : Handlebars.templates[ 'template-history-titleLink' ], + hdaSummary : Handlebars.templates[ 'template-history-hdaSummary' ], + downloadLinks : Handlebars.templates[ 'template-history-downloadLinks' ], + failedMetadata : Handlebars.templates[ 'template-history-failedMetaData' ], + tagArea : Handlebars.templates[ 'template-history-tagArea' ], + annotationArea : Handlebars.templates[ 'template-history-annotationArea' ], + displayApps : Handlebars.templates[ 'template-history-displayApps' ] +}; //============================================================================== var HistoryCollection = Backbone.Collection.extend({ @@ -867,22 +874,51 @@ }); //------------------------------------------------------------------------------ +// view for the HistoryCollection (as per current right hand panel) +//var HistoryView = BaseView.extend( LoggableMixin ).extend( UsesStorageMixin ) .extend({ var HistoryView = BaseView.extend( LoggableMixin ).extend({ - // view for the HistoryCollection (as per current right hand panel) // uncomment this out see log messages //logger : console, // direct attachment to existing element el : 'body.historyPage', - + //TODO: add id? + initialize : function(){ this.log( this + '.initialize:', this ); - this.itemViews = []; - var parent = this; + // data that needs to be persistant over page refreshes + this.storage = new PersistantStorage( + 'HistoryView.' + this.model.get( 'id' ), + { visibleItems : {} } + ); + // set up the individual history items/datasets + this.initializeItems(); + }, + + initializeItems : function(){ + this.itemViews = {}; + var historyPanel = this; this.model.items.each( function( item ){ - var itemView = new HistoryItemView({ model: item }); - parent.itemViews.push( itemView ); + var itemId = item.get( 'id' ), + itemView = new HistoryItemView({ + model: item, visible: + historyPanel.storage.get( 'visibleItems' ).get( itemId ) + }); + historyPanel.setUpItemListeners( itemView ); + historyPanel.itemViews[ itemId ] = itemView; + }); + }, + + setUpItemListeners : function( itemView ){ + var HistoryPanel = this; + // use storage to maintain a list of items whose bodies are visible + itemView.bind( 'toggleBodyVisibility', function( id, visible ){ + if( visible ){ + HistoryPanel.storage.get( 'visibleItems' ).set( id, true ); + } else { + HistoryPanel.storage.get( 'visibleItems' ).deleteKey( id ); + } }); }, @@ -907,8 +943,8 @@ var div = $( '<div/>' ), view = this; //NOTE!: render in reverse (newest on top) via prepend (instead of append) - _.each( this.itemViews, function( itemView ){ - view.log( view + '.render_items:', itemView ); + _.each( this.itemViews, function( itemView, viewId ){ + view.log( view + '.render_items:', viewId, itemView ); div.prepend( itemView.render() ); }); return div; @@ -919,12 +955,9 @@ return 'HistoryView(' + nameString + ')'; } }); -//HistoryItemView.templates = InDomTemplateLoader.getTemplates({ -HistoryView.templates = CompiledTemplateLoader.getTemplates({ - 'history-templates.html' : { - historyPanel : 'template-history-historyPanel' - } -}); +HistoryView.templates = { + historyPanel : Handlebars.templates[ 'template-history-historyPanel' ] +}; diff -r c456d67423b6988de9f9777d9758901845d02de8 -r 724aaf15bcbe4989450af00c83ea8538c6b2f051 templates/root/alternate_history.mako --- a/templates/root/alternate_history.mako +++ b/templates/root/alternate_history.mako @@ -344,7 +344,9 @@ ${parent.javascripts()} ${h.js( - "libs/jquery/jstorage", "libs/jquery/jquery.autocomplete", "galaxy.autocom_tagging", + "libs/jquery/jstorage", + "libs/jquery/jquery.autocomplete", "galaxy.autocom_tagging", + "libs/json2", "mvc/base-mvc", "mvc/ui" )} @@ -395,10 +397,10 @@ if( pageData.hdaId ){ self.location = "#" + pageData.hdaId; } - - glx_history = new History( pageData.history ).loadDatasetsAsHistoryItems( pageData.hdas ); - glx_history_view = new HistoryView({ model: glx_history }); + var glx_history = new History( pageData.history ).loadDatasetsAsHistoryItems( pageData.hdas ), + glx_history_view = new HistoryView({ model: glx_history }); glx_history_view.render(); + window.glx_history = glx_history; window.glx_history_view = glx_history_view; return; @@ -452,4 +454,4 @@ ${_('Galaxy History')} </%def> -<body class="historyPage"></body> \ No newline at end of file +<body class="historyPage"></body> Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.
participants (1)
-
Bitbucket