PageRenderTime 62ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/client/galaxy/scripts/mvc/history/history-panel.js

https://bitbucket.org/remy_d1/galaxy-central-manageapi
JavaScript | 662 lines | 414 code | 72 blank | 176 comment | 47 complexity | 3d5f2baedea0cd237dd7780f9c128ac2 MD5 | raw file
Possible License(s): CC-BY-3.0
  1. define([
  2. "mvc/list/list-panel",
  3. "mvc/history/history-model",
  4. "mvc/history/history-contents",
  5. "mvc/history/hda-li",
  6. "mvc/history/hdca-li",
  7. "mvc/collection/collection-panel",
  8. "mvc/user/user-model",
  9. "mvc/base-mvc",
  10. "utils/localization"
  11. ], function(
  12. LIST_PANEL,
  13. HISTORY_MODEL,
  14. HISTORY_CONTENTS,
  15. HDA_LI,
  16. HDCA_LI,
  17. COLLECTION_PANEL,
  18. USER,
  19. BASE_MVC,
  20. _l
  21. ){
  22. // ============================================================================
  23. /** session storage for individual history preferences */
  24. var HistoryPrefs = BASE_MVC.SessionStorageModel.extend(
  25. /** @lends HistoryPrefs.prototype */{
  26. //TODO:?? possibly mark as current T/F - have History.currId() (a class method) return that value
  27. defaults : {
  28. //TODO:?? expandedIds to array?
  29. expandedIds : {},
  30. //TODO:?? move to user?
  31. show_deleted : false,
  32. show_hidden : false
  33. //TODO: add scroll position?
  34. },
  35. /** add an hda id to the hash of expanded hdas */
  36. addExpanded : function( model ){
  37. var key = 'expandedIds';
  38. //TODO:?? is this right anymore?
  39. this.save( key, _.extend( this.get( key ), _.object([ model.id ], [ model.get( 'id' ) ]) ) );
  40. },
  41. /** remove an hda id from the hash of expanded hdas */
  42. removeExpanded : function( model ){
  43. var key = 'expandedIds';
  44. this.save( key, _.omit( this.get( key ), model.id ) );
  45. },
  46. toString : function(){
  47. return 'HistoryPrefs(' + this.id + ')';
  48. }
  49. });
  50. // class lvl for access w/o instantiation
  51. HistoryPrefs.storageKeyPrefix = 'history:';
  52. /** key string to store each histories settings under */
  53. HistoryPrefs.historyStorageKey = function historyStorageKey( historyId ){
  54. if( !historyId ){
  55. throw new Error( 'HistoryPrefs.historyStorageKey needs valid id: ' + historyId );
  56. }
  57. // single point of change
  58. return ( HistoryPrefs.storageKeyPrefix + historyId );
  59. };
  60. /** return the existing storage for the history with the given id (or create one if it doesn't exist) */
  61. HistoryPrefs.get = function get( historyId ){
  62. return new HistoryPrefs({ id: HistoryPrefs.historyStorageKey( historyId ) });
  63. };
  64. /** clear all history related items in sessionStorage */
  65. HistoryPrefs.clearAll = function clearAll( historyId ){
  66. for( var key in sessionStorage ){
  67. if( key.indexOf( HistoryPrefs.storageKeyPrefix ) === 0 ){
  68. sessionStorage.removeItem( key );
  69. }
  70. }
  71. };
  72. /* =============================================================================
  73. TODO:
  74. ============================================================================= */
  75. /** @class non-editable, read-only View/Controller for a history model.
  76. * Allows:
  77. * changing the loaded history
  78. * displaying data, info, and download
  79. * tracking history attrs: size, tags, annotations, name, etc.
  80. * Does not allow:
  81. * changing the name
  82. */
  83. var _super = LIST_PANEL.ModelListPanel;
  84. var HistoryPanel = _super.extend(
  85. /** @lends HistoryPanel.prototype */{
  86. /** logger used to record this.log messages, commonly set to console */
  87. //logger : console,
  88. /** class to use for constructing the HDA views */
  89. HDAViewClass : HDA_LI.HDAListItemView,
  90. /** class to use for constructing the HDCA views */
  91. HDCAViewClass : HDCA_LI.HDCAListItemView,
  92. /** class to used for constructing collection of sub-view models */
  93. collectionClass : HISTORY_CONTENTS.HistoryContents,
  94. /** key of attribute in model to assign to this.collection */
  95. modelCollectionKey : 'contents',
  96. tagName : 'div',
  97. className : _super.prototype.className + ' history-panel',
  98. /** string to display when the collection is empty */
  99. emptyMsg : _l( 'This history is empty' ),
  100. /** displayed when no items match the search terms */
  101. noneFoundMsg : _l( 'No matching datasets found' ),
  102. /** string used for search placeholder */
  103. searchPlaceholder : _l( 'search datasets' ),
  104. // ......................................................................... SET UP
  105. /** Set up the view, bind listeners.
  106. * @param {Object} attributes optional settings for the panel
  107. */
  108. initialize : function( attributes ){
  109. _super.prototype.initialize.call( this, attributes );
  110. // ---- instance vars
  111. // control contents/behavior based on where (and in what context) the panel is being used
  112. /** where should pages from links be displayed? (default to new tab/window) */
  113. this.linkTarget = attributes.linkTarget || '_blank';
  114. },
  115. /** In this override, clear the update timer on the model */
  116. freeModel : function(){
  117. _super.prototype.freeModel.call( this );
  118. //TODO: move to History.free()
  119. if( this.model ){
  120. this.model.clearUpdateTimeout();
  121. }
  122. return this;
  123. },
  124. /** create any event listeners for the panel
  125. * @fires: rendered:initial on the first render
  126. * @fires: empty-history when switching to a history with no contents or creating a new history
  127. */
  128. _setUpListeners : function(){
  129. _super.prototype._setUpListeners.call( this );
  130. this.on( 'error', function( model, xhr, options, msg, details ){
  131. this.errorHandler( model, xhr, options, msg, details );
  132. });
  133. this.on( 'loading-done', function(){
  134. //TODO:?? if( this.collection.length ){
  135. if( !this.views.length ){
  136. this.trigger( 'empty-history', this );
  137. }
  138. });
  139. },
  140. // ------------------------------------------------------------------------ loading history/hda models
  141. //NOTE: all the following fns replace the existing history model with a new model
  142. // (in the following 'details' refers to the full set of contents api data (urls, display_apps, misc_info, etc.)
  143. // - contents w/o details will have summary data only (name, hid, deleted, visible, state, etc.))
  144. //TODO: too tangled...
  145. /** loads a history & contents, getting details of any contents whose ids are stored in sessionStorage
  146. * (but does not make them the current history)
  147. */
  148. loadHistoryWithDetails : function( historyId, attributes, historyFn, contentsFn ){
  149. this.info( 'loadHistoryWithDetails:', historyId, attributes, historyFn, contentsFn );
  150. var detailIdsFn = function( historyData ){
  151. // will be called to get content ids that need details from the api
  152. //TODO:! non-visible contents are getting details loaded... either stop loading them at all or filter ids thru isVisible
  153. return _.values( HistoryPrefs.get( historyData.id ).get( 'expandedIds' ) );
  154. };
  155. return this.loadHistory( historyId, attributes, historyFn, contentsFn, detailIdsFn );
  156. },
  157. /** loads a history & contents (but does not make them the current history) */
  158. loadHistory : function( historyId, attributes, historyFn, contentsFn, detailIdsFn ){
  159. this.info( 'loadHistory:', historyId, attributes, historyFn, contentsFn, detailIdsFn );
  160. var panel = this;
  161. attributes = attributes || {};
  162. panel.trigger( 'loading', panel );
  163. //this.info( 'loadHistory:', historyId, attributes, historyFn, contentsFn, detailIdsFn );
  164. var xhr = HISTORY_MODEL.History.getHistoryData( historyId, {
  165. historyFn : historyFn,
  166. contentsFn : contentsFn,
  167. detailIdsFn : attributes.initiallyExpanded || detailIdsFn
  168. });
  169. return panel._loadHistoryFromXHR( xhr, attributes )
  170. .fail( function( xhr, where, history ){
  171. // throw an error up for the error handler
  172. panel.trigger( 'error', panel, xhr, attributes, _l( 'An error was encountered while ' + where ),
  173. { historyId: historyId, history: history || {} });
  174. })
  175. .always( function(){
  176. // bc _hideLoadingIndicator relies on this firing
  177. panel.trigger( 'loading-done', panel );
  178. });
  179. },
  180. /** given an xhr that will provide both history and contents data, pass data to set model or handle xhr errors */
  181. _loadHistoryFromXHR : function( xhr, attributes ){
  182. var panel = this;
  183. xhr.then( function( historyJSON, contentsJSON ){
  184. panel.JSONToModel( historyJSON, contentsJSON, attributes );
  185. panel.render();
  186. });
  187. xhr.fail( function( xhr, where ){
  188. // render anyways - whether we get a model or not
  189. panel.render();
  190. });
  191. return xhr;
  192. },
  193. /** convenience alias to the model. Updates the item list only (not the history) */
  194. refreshContents : function( detailIds, options ){
  195. if( this.model ){
  196. return this.model.refresh( detailIds, options );
  197. }
  198. // may have callbacks - so return an empty promise
  199. return $.when();
  200. },
  201. //TODO:?? seems unneccesary
  202. //TODO: Maybe better in History?
  203. /** create a new history model from JSON and call setModel on it */
  204. JSONToModel : function( newHistoryJSON, newHdaJSON, attributes ){
  205. this.log( 'JSONToModel:', newHistoryJSON, newHdaJSON, attributes );
  206. attributes = attributes || {};
  207. //this.log( 'JSONToModel:', newHistoryJSON, newHdaJSON.length, attributes );
  208. var model = new HISTORY_MODEL.History( newHistoryJSON, newHdaJSON, attributes );
  209. //TODO:?? here?
  210. this.setModel( model );
  211. return model;
  212. },
  213. /** release/free/shutdown old models and set up panel for new models
  214. * @fires new-model with the panel as parameter
  215. */
  216. setModel : function( model, attributes ){
  217. attributes = attributes || {};
  218. _super.prototype.setModel.call( this, model, attributes );
  219. if( this.model ){
  220. this._setUpWebStorage( attributes.initiallyExpanded, attributes.show_deleted, attributes.show_hidden );
  221. }
  222. },
  223. // ------------------------------------------------------------------------ browser stored prefs
  224. /** Set up client side storage. Currently PersistanStorage keyed under 'HistoryPanel.<id>'
  225. * @param {Object} initiallyExpanded
  226. * @param {Boolean} show_deleted whether to show deleted contents (overrides stored)
  227. * @param {Boolean} show_hidden
  228. * @see PersistentStorage
  229. */
  230. _setUpWebStorage : function( initiallyExpanded, show_deleted, show_hidden ){
  231. //if( !this.model ){ return this; }
  232. //this.log( '_setUpWebStorage', initiallyExpanded, show_deleted, show_hidden );
  233. if( this.storage ){
  234. this.stopListening( this.storage );
  235. }
  236. this.storage = new HistoryPrefs({
  237. id: HistoryPrefs.historyStorageKey( this.model.get( 'id' ) )
  238. });
  239. // expandedIds is a map of content.ids -> a boolean repr'ing whether that item's body is already expanded
  240. // store any pre-expanded ids passed in
  241. if( _.isObject( initiallyExpanded ) ){
  242. this.storage.set( 'expandedIds', initiallyExpanded );
  243. }
  244. // get the show_deleted/hidden settings giving priority to values passed in, using web storage otherwise
  245. // if the page has specifically requested show_deleted/hidden, these will be either true or false
  246. // (as opposed to undefined, null) - and we give priority to that setting
  247. if( _.isBoolean( show_deleted ) ){
  248. this.storage.set( 'show_deleted', show_deleted );
  249. }
  250. if( _.isBoolean( show_hidden ) ){
  251. this.storage.set( 'show_hidden', show_hidden );
  252. }
  253. this.trigger( 'new-storage', this.storage, this );
  254. this.log( this + ' (init\'d) storage:', this.storage.get() );
  255. this.listenTo( this.storage, {
  256. 'change:show_deleted' : function( view, newVal ){
  257. this.showDeleted = newVal;
  258. },
  259. 'change:show_hidden' : function( view, newVal ){
  260. this.showHidden = newVal;
  261. }
  262. }, this );
  263. this.showDeleted = ( show_deleted !== undefined )? show_deleted : this.storage.get( 'show_deleted' );
  264. this.showHidden = ( show_hidden !== undefined )? show_hidden : this.storage.get( 'show_hidden' );
  265. return this;
  266. },
  267. // ------------------------------------------------------------------------ panel rendering
  268. /** In this override, add a btn to toggle the selectors */
  269. _buildNewRender : function(){
  270. var $newRender = _super.prototype._buildNewRender.call( this );
  271. if( this.multiselectActions.length ){
  272. $newRender.find( '.controls .actions' ).prepend( this._renderSelectButton() );
  273. }
  274. return $newRender;
  275. },
  276. /** button for starting select mode */
  277. _renderSelectButton : function( $where ){
  278. return faIconButton({
  279. title : _l( 'Operations on multiple datasets' ),
  280. classes : 'show-selectors-btn',
  281. faIcon : 'fa-check-square-o'
  282. });
  283. },
  284. // ------------------------------------------------------------------------ sub-views
  285. /** In this override, since history contents are mixed,
  286. * get the appropo view class based on history_content_type
  287. */
  288. _getItemViewClass : function( model ){
  289. var contentType = model.get( "history_content_type" );
  290. switch( contentType ){
  291. case 'dataset':
  292. return this.HDAViewClass;
  293. case 'dataset_collection':
  294. return this.HDCAViewClass;
  295. }
  296. throw new TypeError( 'Unknown history_content_type: ' + contentType );
  297. },
  298. /** in this override, check if the contents would also display based on show_deleted/hidden */
  299. _filterItem : function( model ){
  300. var panel = this;
  301. return ( _super.prototype._filterItem.call( panel, model )
  302. && ( !model.hidden() || panel.showHidden )
  303. && ( !model.isDeletedOrPurged() || panel.showDeleted ) );
  304. },
  305. /** in this override, add a linktarget, and expand if id is in web storage */
  306. _getItemViewOptions : function( model ){
  307. var options = _super.prototype._getItemViewOptions.call( this, model );
  308. return _.extend( options, {
  309. linkTarget : this.linkTarget,
  310. expanded : !!this.storage.get( 'expandedIds' )[ model.id ],
  311. hasUser : this.model.ownedByCurrUser()
  312. });
  313. },
  314. /** In this override, add/remove expanded/collapsed model ids to/from web storage */
  315. _setUpItemViewListeners : function( view ){
  316. var panel = this;
  317. _super.prototype._setUpItemViewListeners.call( panel, view );
  318. //TODO:?? could use 'view:expanded' here?
  319. // maintain a list of items whose bodies are expanded
  320. view.on( 'expanded', function( v ){
  321. panel.storage.addExpanded( v.model );
  322. });
  323. view.on( 'collapsed', function( v ){
  324. panel.storage.removeExpanded( v.model );
  325. });
  326. return this;
  327. },
  328. // ------------------------------------------------------------------------ selection
  329. /** Override to correctly set the historyId of the new collection */
  330. getSelectedModels : function(){
  331. var collection = _super.prototype.getSelectedModels.call( this );
  332. collection.historyId = this.collection.historyId;
  333. return collection;
  334. },
  335. // ------------------------------------------------------------------------ panel events
  336. /** event map */
  337. events : _.extend( _.clone( _super.prototype.events ), {
  338. // toggle list item selectors
  339. 'click .show-selectors-btn' : 'toggleSelectors'
  340. // allow (error) messages to be clicked away
  341. //TODO: switch to common close (X) idiom
  342. //'click .messages' : 'clearMessages',
  343. //TODO: remove
  344. //'click .history-search-btn' : 'toggleSearchControls'
  345. }),
  346. /** Handle the user toggling the deleted visibility by:
  347. * (1) storing the new value in the persistent storage
  348. * (2) re-rendering the history
  349. * @returns {Boolean} new show_deleted setting
  350. */
  351. toggleShowDeleted : function( show, store ){
  352. show = ( show !== undefined )?( show ):( !this.showDeleted );
  353. store = ( store !== undefined )?( store ):( true );
  354. this.showDeleted = show;
  355. if( store ){
  356. this.storage.set( 'show_deleted', show );
  357. }
  358. this.trigger( 'show-hidden', show );
  359. //TODO:?? to events on storage('change:show_deleted')
  360. this.renderItems();
  361. return this.showDeleted;
  362. },
  363. /** Handle the user toggling the deleted visibility by:
  364. * (1) storing the new value in the persistent storage
  365. * (2) re-rendering the history
  366. * @returns {Boolean} new show_hidden setting
  367. */
  368. toggleShowHidden : function( show, store ){
  369. show = ( show !== undefined )?( show ):( !this.showHidden );
  370. store = ( store !== undefined )?( store ):( true );
  371. this.showHidden = show;
  372. if( store ){
  373. this.storage.set( 'show_hidden', show );
  374. }
  375. this.trigger( 'show-hidden', show );
  376. //TODO:?? to events on storage('change:show_deleted')
  377. this.renderItems();
  378. return this.showHidden;
  379. },
  380. /** On the first search, if there are no details - load them, then search */
  381. _firstSearch : function( searchFor ){
  382. var panel = this,
  383. inputSelector = '.history-search-input';
  384. this.log( 'onFirstSearch', searchFor );
  385. if( panel.model.contents.haveDetails() ){
  386. panel.searchItems( searchFor );
  387. return;
  388. }
  389. panel.$el.find( inputSelector ).searchInput( 'toggle-loading' );
  390. panel.model.contents.fetchAllDetails({ silent: true })
  391. .always( function(){
  392. panel.$el.find( inputSelector ).searchInput( 'toggle-loading' );
  393. })
  394. .done( function(){
  395. panel.searchItems( searchFor );
  396. });
  397. },
  398. //TODO: break this out
  399. // ........................................................................ error handling
  400. /** Event handler for errors (from the panel, the history, or the history's contents)
  401. * @param {Model or View} model the (Backbone) source of the error
  402. * @param {XMLHTTPRequest} xhr any ajax obj. assoc. with the error
  403. * @param {Object} options the options map commonly used with bbone ajax
  404. * @param {String} msg optional message passed to ease error location
  405. * @param {Object} msg optional object containing error details
  406. */
  407. errorHandler : function( model, xhr, options, msg, details ){
  408. this.error( model, xhr, options, msg, details );
  409. //TODO: getting JSON parse errors from jq migrate
  410. // interrupted ajax
  411. if( xhr && xhr.status === 0 && xhr.readyState === 0 ){
  412. // bad gateway
  413. } else if( xhr && xhr.status === 502 ){
  414. //TODO: gmail style 'reconnecting in Ns'
  415. // otherwise, show an error message inside the panel
  416. } else {
  417. // if sentry is available, attempt to get the event id
  418. var parsed = this._parseErrorMessage( model, xhr, options, msg, details );
  419. // it's possible to have a triggered error before the message container is rendered - wait for it to show
  420. if( !this.$messages().is( ':visible' ) ){
  421. this.once( 'rendered', function(){
  422. this.displayMessage( 'error', parsed.message, parsed.details );
  423. });
  424. } else {
  425. this.displayMessage( 'error', parsed.message, parsed.details );
  426. }
  427. }
  428. },
  429. /** Parse an error event into an Object usable by displayMessage based on the parameters
  430. * note: see errorHandler for more info on params
  431. */
  432. _parseErrorMessage : function( model, xhr, options, msg, details, sentryId ){
  433. //if( xhr.responseText ){
  434. // xhr.responseText = _.escape( xhr.responseText );
  435. //}
  436. var user = Galaxy.currUser,
  437. // add the args (w/ some extra info) into an obj
  438. parsed = {
  439. message : this._bePolite( msg ),
  440. details : {
  441. message : msg,
  442. raven : ( window.Raven && _.isFunction( Raven.lastEventId) )?
  443. ( Raven.lastEventId() ):( undefined ),
  444. agent : navigator.userAgent,
  445. // add ajax data from Galaxy object cache
  446. url : ( window.Galaxy )?( Galaxy.lastAjax.url ):( undefined ),
  447. data : ( window.Galaxy )?( Galaxy.lastAjax.data ):( undefined ),
  448. options : ( xhr )?( _.omit( options, 'xhr' ) ):( options ),
  449. xhr : xhr,
  450. source : ( _.isFunction( model.toJSON ) )?( model.toJSON() ):( model + '' ),
  451. user : ( user instanceof USER.User )?( user.toJSON() ):( user + '' )
  452. }
  453. };
  454. // add any extra details passed in
  455. _.extend( parsed.details, details || {} );
  456. // fancy xhr.header parsing (--> obj)
  457. if( xhr && _.isFunction( xhr.getAllResponseHeaders ) ){
  458. var responseHeaders = xhr.getAllResponseHeaders();
  459. responseHeaders = _.compact( responseHeaders.split( '\n' ) );
  460. responseHeaders = _.map( responseHeaders, function( header ){
  461. return header.split( ': ' );
  462. });
  463. parsed.details.xhr.responseHeaders = _.object( responseHeaders );
  464. }
  465. return parsed;
  466. },
  467. /** Modify an error message to be fancy and wear a monocle. */
  468. _bePolite : function( msg ){
  469. msg = msg || _l( 'An error occurred while getting updates from the server' );
  470. return msg + '. ' + _l( 'Please contact a Galaxy administrator if the problem persists' ) + '.';
  471. },
  472. // ........................................................................ (error) messages
  473. /** Display a message in the top of the panel.
  474. * @param {String} type type of message ('done', 'error', 'warning')
  475. * @param {String} msg the message to display
  476. * @param {Object or HTML} modal contents displayed when the user clicks 'details' in the message
  477. */
  478. displayMessage : function( type, msg, details ){
  479. //precondition: msgContainer must have been rendered even if there's no model
  480. var panel = this;
  481. //this.log( 'displayMessage', type, msg, details );
  482. this.scrollToTop();
  483. var $msgContainer = this.$messages(),
  484. $msg = $( '<div/>' ).addClass( type + 'message' ).html( msg );
  485. //this.log( ' ', $msgContainer );
  486. if( !_.isEmpty( details ) ){
  487. var $detailsLink = $( '<a href="javascript:void(0)">Details</a>' )
  488. .click( function(){
  489. Galaxy.modal.show( panel._messageToModalOptions( type, msg, details ) );
  490. return false;
  491. });
  492. $msg.append( ' ', $detailsLink );
  493. }
  494. return $msgContainer.html( $msg );
  495. },
  496. /** convert msg and details into modal options usable by Galaxy.modal */
  497. _messageToModalOptions : function( type, msg, details ){
  498. // only error is fleshed out here
  499. var panel = this,
  500. options = { title: 'Details' };
  501. if( _.isObject( details ) ){
  502. details = _.omit( details, _.functions( details ) );
  503. var text = JSON.stringify( details, null, ' ' ),
  504. pre = $( '<pre/>' ).text( text );
  505. options.body = $( '<div/>' ).append( pre );
  506. } else {
  507. options.body = $( '<div/>' ).html( details );
  508. }
  509. options.buttons = {
  510. 'Ok': function(){
  511. Galaxy.modal.hide();
  512. panel.clearMessages();
  513. }
  514. //TODO: if( type === 'error' ){ options.buttons[ 'Report this error' ] = function(){} }
  515. };
  516. return options;
  517. },
  518. /** Remove all messages from the panel. */
  519. clearMessages : function( ev ){
  520. $( ev.currentTarget ).fadeOut( this.fxSpeed, function(){
  521. $( this ).remove();
  522. });
  523. //this.$messages().children().not( '.quota-message' ).remove();
  524. return this;
  525. },
  526. // ........................................................................ scrolling
  527. /** Scrolls the panel to show the content sub-view with the given hid.
  528. * @param {Integer} hid the hid of item to scroll into view
  529. * @returns {HistoryPanel} the panel
  530. */
  531. scrollToHid : function( hid ){
  532. return this.scrollToItem( _.first( this.viewsWhereModel({ hid: hid }) ) );
  533. },
  534. // ........................................................................ misc
  535. /** Return a string rep of the history */
  536. toString : function(){
  537. return 'HistoryPanel(' + (( this.model )?( this.model.get( 'name' )):( '' )) + ')';
  538. }
  539. });
  540. //------------------------------------------------------------------------------ TEMPLATES
  541. HistoryPanel.prototype.templates = (function(){
  542. var controlsTemplate = BASE_MVC.wrapTemplate([
  543. '<div class="controls">',
  544. '<div class="title">',
  545. '<div class="name"><%= history.name %></div>',
  546. '</div>',
  547. '<div class="subtitle"></div>',
  548. '<div class="history-size"><%= history.nice_size %></div>',
  549. '<div class="actions"></div>',
  550. '<div class="messages">',
  551. '<% if( history.deleted ){ %>',
  552. '<div class="deleted-msg warningmessagesmall">',
  553. _l( 'This history has been deleted' ),
  554. '</div>',
  555. '<% } %>',
  556. '<% if( history.message ){ %>',
  557. // should already be localized
  558. '<div class="<%= history.message.level || "info" %>messagesmall">',
  559. '<%= history.message.text %>',
  560. '</div>',
  561. '<% } %>',
  562. '</div>',
  563. // add tags and annotations
  564. '<div class="tags-display"></div>',
  565. '<div class="annotation-display"></div>',
  566. '<div class="search">',
  567. '<div class="search-input"></div>',
  568. '</div>',
  569. '<div class="list-actions">',
  570. '<div class="btn-group">',
  571. '<button class="select-all btn btn-default"',
  572. 'data-mode="select">', _l( 'All' ), '</button>',
  573. '<button class="deselect-all btn btn-default"',
  574. 'data-mode="select">', _l( 'None' ), '</button>',
  575. '</div>',
  576. '<button class="list-action-popup-btn btn btn-default">',
  577. _l( 'For all selected' ), '...</button>',
  578. '</div>',
  579. '</div>'
  580. ], 'history' );
  581. return _.extend( _.clone( _super.prototype.templates ), {
  582. controls : controlsTemplate
  583. });
  584. }());
  585. //==============================================================================
  586. return {
  587. HistoryPanel: HistoryPanel
  588. };
  589. });