PageRenderTime 52ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/static/scripts/mvc/history/multi-panel.js

https://bitbucket.org/afgane/galaxy-central
JavaScript | 1101 lines | 700 code | 116 blank | 285 comment | 68 complexity | 23900fe1af71e86040afaab155ab48e9 MD5 | raw file
Possible License(s): CC-BY-3.0
  1. define([
  2. "mvc/history/history-model",
  3. "mvc/history/history-panel-edit",
  4. "mvc/base-mvc",
  5. "utils/ajax-queue",
  6. "ui/mode-button",
  7. "ui/search-input"
  8. ], function( HISTORY_MODEL, HPANEL_EDIT, baseMVC, ajaxQueue ){
  9. window.HISTORY_MODEL = HISTORY_MODEL;
  10. //==============================================================================
  11. /** */
  12. function historyCopyDialog( history, options ){
  13. options = options || {};
  14. // fall back to un-notifying copy
  15. if( !( Galaxy && Galaxy.modal ) ){
  16. return history.copy();
  17. }
  18. // maybe better as multiselect dialog?
  19. var historyName = history.get( 'name' ),
  20. defaultCopyName = "Copy of '" + historyName + "'";
  21. function validateName( name ){
  22. if( !name ){
  23. if( !Galaxy.modal.$( '#invalid-title' ).size() ){
  24. var $invalidTitle = $( '<p/>' ).attr( 'id', 'invalid-title' )
  25. .css({ color: 'red', 'margin-top': '8px' })
  26. .addClass( 'bg-danger' ).text( _l( 'Please enter a valid history title' ) );
  27. Galaxy.modal.$( '.modal-body' ).append( $invalidTitle );
  28. }
  29. return false;
  30. }
  31. return name;
  32. }
  33. function copyHistory( name ){
  34. var $copyIndicator = $( '<p><span class="fa fa-spinner fa-spin"></span> Copying history...</p>' )
  35. .css( 'margin-top', '8px' );
  36. Galaxy.modal.$( '.modal-body' ).append( $copyIndicator );
  37. history.copy( true, name )
  38. //TODO: make this unneccessary with pub-sub error
  39. .fail( function(){
  40. alert( _l( 'History could not be copied. Please contact a Galaxy administrator' ) );
  41. })
  42. .always( function(){
  43. Galaxy.modal.hide();
  44. });
  45. }
  46. function checkNameAndCopy(){
  47. var name = Galaxy.modal.$( '#copy-modal-title' ).val();
  48. if( !validateName( name ) ){ return; }
  49. copyHistory( name );
  50. }
  51. Galaxy.modal.show( _.extend({
  52. title : _l( 'Copying history' ) + ' "' + historyName + '"',
  53. body : $([
  54. '<label for="copy-modal-title">',
  55. _l( 'Enter a title for the copied history' ), ':',
  56. '</label><br />',
  57. '<input id="copy-modal-title" class="form-control" style="width: 100%" value="',
  58. defaultCopyName, '" />'
  59. ].join('')),
  60. buttons : {
  61. 'Cancel' : function(){ Galaxy.modal.hide(); },
  62. 'Copy' : checkNameAndCopy
  63. }
  64. }, options ));
  65. $( '#copy-modal-title' ).focus().select();
  66. $( '#copy-modal-title' ).on( 'keydown', function( ev ){
  67. if( ev.keyCode === 13 ){
  68. checkNameAndCopy();
  69. }
  70. });
  71. }
  72. /* ==============================================================================
  73. TODO:
  74. rendering/delayed rendering is a mess
  75. may not need to render columns in renderColumns (just moving them/attaching them may be enough)
  76. copy places old current in wrong location
  77. sort by size is confusing (but correct) - nice_size shows actual disk usage, !== size
  78. handle delete current
  79. currently manual by user - delete then create new - make one step
  80. render
  81. move all columns over as one (agg. then html())
  82. performance
  83. rendering columns could be better
  84. lazy load history list (for large numbers)
  85. req. API limit/offset
  86. move in-view from pubsub
  87. handle errors
  88. handle no histories
  89. handle no histories found
  90. handle anon
  91. no button
  92. error message on loading url direct
  93. include hidden/deleted
  94. allow toggling w/o setting in storage
  95. reloading with expanded collections doesn't get details of that collection
  96. change includeDeleted to an ajax call
  97. better narrowing
  98. privatize non-interface fns
  99. search for and drag and drop - should reset after dataset is loaded (or alt. clear search)
  100. ?columns should be a panel, not have a panel
  101. ============================================================================== */
  102. /** @class A container for a history panel that renders controls for that history (delete, copy, etc.)
  103. */
  104. var HistoryPanelColumn = Backbone.View.extend( baseMVC.LoggableMixin ).extend({
  105. //TODO: extend from panel? (instead of aggregating)
  106. //logger : console,
  107. tagName : 'div',
  108. className : 'history-column flex-column flex-row-container',
  109. id : function id(){
  110. if( !this.model ){ return ''; }
  111. return 'history-column-' + this.model.get( 'id' );
  112. },
  113. // ------------------------------------------------------------------------ set up
  114. /** set up passed-in panel (if any) and listeners */
  115. initialize : function initialize( options ){
  116. options = options || {};
  117. //this.log( this + '.init', options );
  118. // if model, set up model
  119. // create panel sub-view
  120. //TODO: use current-history-panel for current
  121. this.panel = options.panel || this.createPanel( options );
  122. this.setUpListeners();
  123. },
  124. /** create a history panel for this column */
  125. createPanel : function createPanel( panelOptions ){
  126. panelOptions = _.extend({
  127. model : this.model,
  128. //el : this.$panel(),
  129. // non-current panels should set their hdas to draggable
  130. dragItems : true
  131. }, panelOptions );
  132. //this.log( 'panelOptions:', panelOptions );
  133. //TODO: use current-history-panel for current
  134. var panel = new HPANEL_EDIT.HistoryPanelEdit( panelOptions );
  135. panel._renderEmptyMessage = this.__patch_renderEmptyMessage;
  136. return panel;
  137. },
  138. //TODO: needs work
  139. //TODO: move to stub-class to avoid monkey-patching
  140. /** the function monkey-patched into panels to show contents loading state */
  141. __patch_renderEmptyMessage : function( $whereTo ){
  142. var panel = this,
  143. hdaCount = _.chain( this.model.get( 'state_ids' ) ).values().flatten().value().length,
  144. $emptyMsg = panel.$emptyMessage( $whereTo );
  145. if( !_.isEmpty( panel.hdaViews ) ){
  146. $emptyMsg.hide();
  147. } else if( hdaCount && !this.model.contents.length ){
  148. $emptyMsg.empty()
  149. .append( $( '<span class="fa fa-spinner fa-spin"></span> <i>loading datasets...</i>' ) ).show();
  150. } else if( panel.searchFor ){
  151. $emptyMsg.text( panel.noneFoundMsg ).show();
  152. } else {
  153. $emptyMsg.text( panel.emptyMsg ).show();
  154. }
  155. return $emptyMsg;
  156. },
  157. /** set up reflexive listeners */
  158. setUpListeners : function setUpListeners(){
  159. var column = this;
  160. //this.log( 'setUpListeners', this );
  161. this.once( 'rendered', function(){
  162. column.trigger( 'rendered:initial', column );
  163. });
  164. this.setUpPanelListeners();
  165. },
  166. /** set listeners needed for panel */
  167. setUpPanelListeners : function setUpPanelListeners(){
  168. var column = this;
  169. this.listenTo( this.panel, {
  170. //'all': function(){ console.info( 'panel of ' + this, arguments ); },
  171. // assumes panel will take the longest to render
  172. 'rendered': function(){
  173. column.trigger( 'rendered', column );
  174. }
  175. }, this );
  176. },
  177. /** do the dimensions of this column overlap the given (horizontal) browser coords? */
  178. inView : function( viewLeft, viewRight ){
  179. //TODO: offset is expensive
  180. var columnLeft = this.$el.offset().left,
  181. columnRight = columnLeft + this.$el.width();
  182. if( columnRight < viewLeft ){ return false; }
  183. if( columnLeft > viewRight ){ return false; }
  184. return true;
  185. },
  186. /** shortcut to the panel */
  187. $panel : function $panel(){
  188. return this.$( '.history-panel' );
  189. },
  190. // ------------------------------------------------------------------------ render
  191. /** render ths column, its panel, and set up plugins */
  192. render : function render( speed ){
  193. speed = ( speed !== undefined )?( speed ):( 'fast' );
  194. //this.log( this + '.render', this.$el, this.el );
  195. //TODO: not needed
  196. var modelData = this.model? this.model.toJSON(): {};
  197. this.$el.html( this.template( modelData ) );
  198. this.renderPanel( speed );
  199. // if model and not children
  200. // template
  201. // render controls
  202. this.setUpBehaviors();
  203. // add panel
  204. return this;
  205. },
  206. /** set up plugins */
  207. setUpBehaviors : function setUpBehaviors(){
  208. //this.log( 'setUpBehaviors:', this );
  209. //var column = this;
  210. // on panel size change, ...
  211. },
  212. /** column body template with inner div for panel based on data (model json) */
  213. template : function template( data ){
  214. data = data || {};
  215. var html = [
  216. '<div class="panel-controls clear flex-row">',
  217. this.controlsLeftTemplate(),
  218. //'<button class="btn btn-default">Herp</button>',
  219. '<div class="pull-right">',
  220. '<button class="delete-history btn btn-default">',
  221. data.deleted? _l( 'Undelete' ): _l( 'Delete' ),
  222. '</button>',
  223. '<button class="copy-history btn btn-default">', _l( 'Copy' ), '</button>',
  224. '</div>',
  225. '</div>',
  226. '<div class="inner flex-row flex-column-container">',
  227. '<div id="history-', data.id, '" class="history-column history-panel flex-column"></div>',
  228. '</div>'
  229. ].join( '' );
  230. return $( html );
  231. },
  232. /** controls template displaying controls above the panel based on this.currentHistory */
  233. controlsLeftTemplate : function(){
  234. return ( this.currentHistory )?
  235. [
  236. '<div class="pull-left">',
  237. '<button class="create-new btn btn-default">', _l( 'Create new' ), '</button> ',
  238. '</div>'
  239. ].join( '' )
  240. :[
  241. '<div class="pull-left">',
  242. '<button class="switch-to btn btn-default">', _l( 'Switch to' ), '</button>',
  243. '</div>'
  244. ].join( '' );
  245. },
  246. /** render the panel contained in the column using speed for fx speed */
  247. renderPanel : function renderPanel( speed ){
  248. speed = ( speed !== undefined )?( speed ):( 'fast' );
  249. this.panel.setElement( this.$panel() ).render( speed );
  250. return this;
  251. },
  252. // ------------------------------------------------------------------------ behaviors and events
  253. /** event map */
  254. events : {
  255. // will make this the current history
  256. 'click .switch-to.btn' : function(){ this.model.setAsCurrent(); },
  257. // toggles deleted here and on the server and re-renders
  258. 'click .delete-history.btn' : function(){
  259. var column = this,
  260. xhr;
  261. if( this.model.get( 'deleted' ) ){
  262. xhr = this.model.undelete();
  263. } else {
  264. xhr = this.model._delete();
  265. }
  266. //TODO: better error handler
  267. xhr.fail( function( xhr, status, error ){
  268. alert( _l( 'Could not delete the history' ) + ':\n' + error );
  269. })
  270. .done( function( data ){
  271. column.render();
  272. });
  273. },
  274. // will copy this history and make the copy the current history
  275. 'click .copy-history.btn' : 'copy'
  276. },
  277. // ------------------------------------------------------------------------ non-current controls
  278. /** Open a modal to get a new history name, copy it (if not canceled), and makes the copy current */
  279. copy : function copy(){
  280. historyCopyDialog( this.model );
  281. },
  282. // ------------------------------------------------------------------------ misc
  283. /** String rep */
  284. toString : function(){
  285. return 'HistoryPanelColumn(' + ( this.panel? this.panel : '' ) + ')';
  286. }
  287. });
  288. //==============================================================================
  289. /** @class A view of a HistoryCollection and displays histories similarly to the current history panel.
  290. */
  291. var MultiPanelColumns = Backbone.View.extend( baseMVC.LoggableMixin ).extend({
  292. //logger : console,
  293. // ------------------------------------------------------------------------ set up
  294. /** Set up internals, history collection, and columns to display the history */
  295. initialize : function initialize( options ){
  296. options = options || {};
  297. this.log( this + '.init', options );
  298. // --- instance vars
  299. if( !options.currentHistoryId ){
  300. throw new Error( this + ' requires a currentHistoryId in the options' );
  301. }
  302. this.currentHistoryId = options.currentHistoryId;
  303. //TODO: move these to some defaults
  304. this.options = {
  305. columnWidth : 312,
  306. borderWidth : 1,
  307. columnGap : 8,
  308. headerHeight : 29,
  309. footerHeight : 0,
  310. controlsHeight : 20
  311. };
  312. /** the order that the collection is rendered in */
  313. this.order = options.order || 'update';
  314. /** named ajax queue for loading hdas */
  315. this.hdaQueue = new ajaxQueue.NamedAjaxQueue( [], false );
  316. // --- set up models, sub-views, and listeners
  317. /** the original unfiltered and unordered collection of histories */
  318. this.collection = null;
  319. this.setCollection( options.histories || [] );
  320. /** model id to column map */
  321. this.columnMap = {};
  322. //TODO: why create here?
  323. this.createColumns( options.columnOptions );
  324. this.setUpListeners();
  325. },
  326. /** Set up reflexive listeners */
  327. setUpListeners : function setUpListeners(){
  328. //var multipanel = this;
  329. //multipanel.log( 'setUpListeners', multipanel );
  330. },
  331. // ------------------------------------------------------------------------ collection
  332. /** Set up a (new) history collection, sorting and adding listeners
  333. * @fires 'new-collection' when set with this view as the arg
  334. */
  335. setCollection : function setCollection( models ){
  336. var multipanel = this;
  337. multipanel.stopListening( multipanel.collection );
  338. multipanel.collection = models;
  339. //TODO: slow... esp. on start up
  340. //if( multipanel.order !== 'update' ){
  341. multipanel.sortCollection( multipanel.order, { silent: true });
  342. //}
  343. multipanel.setUpCollectionListeners();
  344. multipanel.trigger( 'new-collection', multipanel );
  345. return multipanel;
  346. },
  347. /** Set up listeners for the collection - handling: added histories, change of current, deletion, and sorting */
  348. setUpCollectionListeners : function(){
  349. var multipanel = this,
  350. collection = multipanel.collection;
  351. multipanel.listenTo( collection, {
  352. // handle addition of histories, triggered by column copy and create new
  353. 'add': multipanel.addAsCurrentColumn,
  354. // handle setting a history as current, triggered by history.setAsCurrent
  355. 'set-as-current': multipanel.setCurrentHistory,
  356. // handle deleting a history (depends on whether panels is including deleted or not)
  357. 'change:deleted': multipanel.handleDeletedHistory,
  358. 'sort' : function(){ multipanel.renderColumns( 0 ); }
  359. // debugging
  360. //'all' : function(){
  361. // console.info( 'collection:', arguments );
  362. //}
  363. });
  364. },
  365. /** Re-render and set currentHistoryId to reflect a new current history */
  366. setCurrentHistory : function setCurrentHistory( history ){
  367. var oldCurrentColumn = this.columnMap[ this.currentHistoryId ];
  368. if( oldCurrentColumn ){
  369. oldCurrentColumn.currentHistory = false;
  370. oldCurrentColumn.$el.height( '' );
  371. }
  372. this.currentHistoryId = history.id;
  373. var newCurrentColumn = this.columnMap[ this.currentHistoryId ];
  374. newCurrentColumn.currentHistory = true;
  375. this.sortCollection();
  376. ////TODO: this actually means these render twice (1st from setCollection) - good enough for now
  377. //if( oldCurrentColumn ){ oldCurrentColumn.render().delegateEvents(); }
  378. //TODO:?? this occasionally causes race with hdaQueue
  379. //newCurrentColumn.panel.render( 'fast' ).delegateEvents();
  380. multipanel._recalcFirstColumnHeight();
  381. return newCurrentColumn;
  382. },
  383. /** Either remove a deleted history or re-render it to show the deleted message
  384. * based on collection.includeDeleted
  385. */
  386. handleDeletedHistory : function handleDeletedHistory( history ){
  387. if( history.get( 'deleted' ) ){
  388. this.log( 'handleDeletedHistory', this.collection.includeDeleted, history );
  389. var multipanel = this;
  390. column = multipanel.columnMap[ history.id ];
  391. if( !column ){ return; }
  392. // if it's the current column, create a new, empty history as the new current
  393. if( column.model.id === this.currentHistoryId ){
  394. //TODO: figuring out the order of async here is tricky - for now let the user handle the two step process
  395. //multipanel.collection.create().done( function(){
  396. // if( !multipanel.collection.includeDeleted ){ multipanel.removeColumn( column, false ); }
  397. //});
  398. } else if( !multipanel.collection.includeDeleted ){
  399. multipanel.removeColumn( column );
  400. }
  401. //TODO: prob. be done better
  402. }
  403. },
  404. /** Sort this collection based on order (defaulting to this.order) a string key
  405. * (sorting the collection will re-render the panel)
  406. */
  407. sortCollection : function( order, options ){
  408. order = order || this.order;
  409. var currentHistoryId = this.currentHistoryId;
  410. //note: h.id !== currentHistoryId allows the sort to put the current history first
  411. switch( order ){
  412. case 'name':
  413. //TODO: we can use a 2 arg version and return 1/0/-1
  414. //this.collection.comparator = function( h1, h2 ){
  415. this.collection.comparator = function( h ){
  416. //TODO: this won't do reverse order well
  417. return [ h.id !== currentHistoryId, h.get( 'name' ).toLowerCase() ];
  418. };
  419. break;
  420. case 'size':
  421. this.collection.comparator = function( h ){
  422. //console.debug( 'name sort', arguments )
  423. return [ h.id !== currentHistoryId, h.get( 'size' ) ];
  424. };
  425. break;
  426. default:
  427. this.collection.comparator = function( h ){
  428. return [ h.id !== currentHistoryId, Date( h.get( 'update_time' ) ) ];
  429. };
  430. }
  431. //NOTE: auto fires 'sort' from collection
  432. this.collection.sort( options );
  433. return this.collection;
  434. },
  435. /** Set the sort order and re-sort */
  436. setOrder : function( order ){
  437. if( [ 'update', 'name', 'size' ].indexOf( order ) === -1 ){
  438. order = 'update';
  439. }
  440. this.order = order;
  441. this.sortCollection();
  442. return this;
  443. },
  444. /** create a new history and set it to current */
  445. create : function( ev ){
  446. return this.collection.create({ current: true });
  447. },
  448. // ------------------------------------------------------------------------ columns
  449. /** create columns from collection */
  450. createColumns : function createColumns( columnOptions ){
  451. columnOptions = columnOptions || {};
  452. var multipanel = this;
  453. // clear column map
  454. this.columnMap = {};
  455. multipanel.collection.each( function( model, i ){
  456. var column = multipanel.createColumn( model, columnOptions );
  457. multipanel.columnMap[ model.id ] = column;
  458. });
  459. },
  460. /** create a column and its panel and set up any listeners to them */
  461. createColumn : function createColumn( history, options ){
  462. // options passed can be re-used, so extend them before adding the model to prevent pollution for the next
  463. options = _.extend( {}, options, { model: history });
  464. var column = new HistoryPanelColumn( options );
  465. if( history.id === this.currentHistoryId ){ column.currentHistory = true; }
  466. this.setUpColumnListeners( column );
  467. return column;
  468. },
  469. sortedFilteredColumns : function( filters ){
  470. filters = filters || this.filters;
  471. if( !filters || !filters.length ){
  472. return this.sortedColumns();
  473. }
  474. var multipanel = this;
  475. return multipanel.sortedColumns().filter( function( column, index ){
  476. var filtered = column.currentHistory || _.every( filters.map( function( filter ){
  477. return filter.call( column );
  478. }));
  479. return filtered;
  480. });
  481. },
  482. sortedColumns : function(){
  483. var multipanel = this;
  484. var sorted = this.collection.map( function( history, index ){
  485. return multipanel.columnMap[ history.id ];
  486. });
  487. return sorted;
  488. },
  489. /** */
  490. addColumn : function add( history, render ){
  491. //console.debug( 'adding column for:', history );
  492. render = render !== undefined? render: true;
  493. var newColumn = this.createColumn( history );
  494. this.columnMap[ history.id ] = newColumn;
  495. if( render ){
  496. this.renderColumns();
  497. }
  498. return newColumn;
  499. },
  500. /** */
  501. addAsCurrentColumn : function add( history ){
  502. //console.log( 'adding current column for:', history );
  503. var multipanel = this,
  504. newColumn = this.addColumn( history, false );
  505. this.setCurrentHistory( history );
  506. newColumn.once( 'rendered', function(){
  507. multipanel.queueHdaFetch( newColumn );
  508. });
  509. return newColumn;
  510. },
  511. /** */
  512. removeColumn : function remove( column, render ){
  513. render = render !== undefined? render : true;
  514. this.log( 'removeColumn', column );
  515. if( !column ){ return; }
  516. var multipanel = this,
  517. widthToRemove = this.options.columnWidth + this.options.columnGap;
  518. column.$el.fadeOut( 'fast', function(){
  519. if( render ){
  520. $( this ).remove();
  521. multipanel.$( '.middle' ).width( multipanel.$( '.middle' ).width() - widthToRemove );
  522. multipanel.checkColumnsInView();
  523. multipanel._recalcFirstColumnHeight();
  524. }
  525. //TODO: to freeColumn (where Columns have freePanel)
  526. multipanel.stopListening( column.panel );
  527. multipanel.stopListening( column );
  528. delete multipanel.columnMap[ column.model.id ];
  529. column.remove();
  530. });
  531. },
  532. /** set up listeners for a column and it's panel - handling: hda lazy-loading, drag and drop */
  533. setUpColumnListeners : function setUpColumnListeners( column ){
  534. var multipanel = this;
  535. multipanel.listenTo( column, {
  536. //'all': function(){ console.info( 'column ' + column + ':', arguments ) },
  537. 'in-view': multipanel.queueHdaFetch
  538. });
  539. multipanel.listenTo( column.panel, {
  540. //'all': function(){ console.info( 'panel ' + column.panel + ':', arguments ) },
  541. 'view:draggable:dragstart': function( ev, view, panel, column ){
  542. multipanel._dropData = JSON.parse( ev.dataTransfer.getData( 'text' ) );
  543. multipanel.currentColumnDropTargetOn();
  544. },
  545. 'view:draggable:dragend': function( ev, view, panel, column ){
  546. multipanel._dropData = null;
  547. multipanel.currentColumnDropTargetOff();
  548. },
  549. 'droptarget:drop': function( ev, data, panel ){
  550. var toCopy = multipanel._dropData.filter( function( json ){
  551. return ( _.isObject( json ) && json.id && json.model_class === 'HistoryDatasetAssociation' );
  552. });
  553. multipanel._dropData = null;
  554. var queue = new ajaxQueue.NamedAjaxQueue();
  555. toCopy.forEach( function( hda ){
  556. queue.add({
  557. name : 'copy-' + hda.id,
  558. fn : function(){
  559. return panel.model.contents.copy( hda.id );
  560. }
  561. });
  562. });
  563. queue.start();
  564. queue.done( function( responses ){
  565. panel.model.fetch();
  566. });
  567. }
  568. });
  569. },
  570. /** conv. fn to count the columns in columnMap */
  571. columnMapLength : function(){
  572. return Object.keys( this.columnMap ).length;
  573. },
  574. // ------------------------------------------------------------------------ render
  575. /** Render this view, columns, and set up view plugins */
  576. render : function render( speed ){
  577. speed = speed !== undefined? speed: this.fxSpeed;
  578. var multipanel = this;
  579. multipanel.log( multipanel + '.render' );
  580. multipanel.$el.html( multipanel.template( multipanel.options ) );
  581. //console.debug( multipanel.$( '.loading-overlay' ).fadeIn( 0 ) );
  582. multipanel.renderColumns( speed );
  583. //console.debug( multipanel.$( '.loading-overlay' ).fadeOut( 'fast' ) );
  584. // set the columns to full height allowed and set up behaviors for thie multipanel
  585. multipanel.setUpBehaviors();
  586. //TODO: wrong - has to wait for columns to render
  587. multipanel.trigger( 'rendered', multipanel );
  588. return multipanel;
  589. },
  590. /** Template - overall structure relies on flex-boxes and is 3 components: header, middle, footer */
  591. template : function template( options ){
  592. options = options || {};
  593. var html = [];
  594. if( this.options.headerHeight ){
  595. html = html.concat([
  596. // a loading overlay
  597. '<div class="loading-overlay flex-row"><div class="loading-overlay-message">loading...</div></div>',
  598. '<div class="header flex-column-container">',
  599. // page & history controls
  600. '<div class="header-control header-control-left flex-column">',
  601. '<button class="done btn btn-default">', _l( 'Done' ), '</button>',
  602. '<button class="include-deleted btn btn-default"></button>',
  603. '<div class="order btn-group">',
  604. '<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">',
  605. _l( 'Order histories by' ) + '... <span class="caret"></span>',
  606. '</button>',
  607. '<ul class="dropdown-menu" role="menu">',
  608. '<li><a href="javascript:void(0);" class="order-update">',
  609. _l( 'Time of last update' ),
  610. '</a></li>',
  611. '<li><a href="javascript:void(0);" class="order-name">',
  612. _l( 'Name' ),
  613. '</a></li>',
  614. '<li><a href="javascript:void(0);" class="order-size">',
  615. _l( 'Size' ),
  616. '</a></li>',
  617. '</ul>',
  618. '</div>',
  619. '<div id="search-histories" class="header-search"></div>',
  620. '</div>',
  621. // feedback
  622. '<div class="header-control header-control-center flex-column">',
  623. '<div class="header-info">',
  624. '</div>',
  625. '</div>',
  626. // dataset controls
  627. '<div class="header-control header-control-right flex-column">',
  628. '<div id="search-datasets" class="header-search"></div>',
  629. '<button id="toggle-deleted" class="btn btn-default">',
  630. _l( 'Include deleted datasets' ),
  631. '</button>',
  632. '<button id="toggle-hidden" class="btn btn-default">',
  633. _l( 'Include hidden datasets' ),
  634. '</button>',
  635. '</div>',
  636. '</div>'
  637. ]);
  638. }
  639. html = html.concat([
  640. // middle - where the columns go
  641. '<div class="outer-middle flex-row flex-row-container">',
  642. '<div class="middle flex-column-container flex-row"></div>',
  643. '</div>',
  644. // footer
  645. '<div class="footer flex-column-container">','</div>'
  646. ]);
  647. return $( html.join( '' ) );
  648. },
  649. /** Render the columns and panels */
  650. renderColumns : function renderColumns( speed ){
  651. speed = speed !== undefined? speed: this.fxSpeed;
  652. //this.log( 'renderColumns:', speed );
  653. // render columns and track the total number rendered, firing an event when all are rendered
  654. var multipanel = this,
  655. sortedAndFiltered = multipanel.sortedFilteredColumns();
  656. //console.log( '\t columnMapLength:', this.columnMapLength(), this.columnMap );
  657. //this.log( '\t sortedAndFiltered:', sortedAndFiltered );
  658. // set up width based on collection size
  659. //console.debug( '(render) width before:', multipanel.$( '.middle' ).width() )
  660. multipanel.$( '.middle' ).width( sortedAndFiltered.length
  661. //TODO: magic number 16 === the amount that safely prevents stacking of columns when adding a new one
  662. * ( this.options.columnWidth + this.options.columnGap ) + this.options.columnGap + 16 );
  663. //console.debug( '(render) width now:', multipanel.$( '.middle' ).width() )
  664. //console.debug( 'sortedAndFiltered:', sortedAndFiltered )
  665. //multipanel.$( '.middle' ).empty();
  666. // this.$( '.middle' ).html( sortedAndFiltered.map( function( column, i ){
  667. // return column.$el.hide();
  668. // }));
  669. // sortedAndFiltered.forEach( function( column, i ){
  670. ////console.debug( 'rendering:', column, i )
  671. // //multipanel.$( '.middle' ).append( column.$el.hide() );
  672. // column.delegateEvents();
  673. ////TODO: current column in-view is never fired
  674. // multipanel.renderColumn( column, speed ).$el.show();
  675. // });
  676. // var $els = sortedAndFiltered.map( function( column, i ){
  677. ////console.debug( 'rendering:', column, i )
  678. // multipanel.renderColumn( column, speed );
  679. // return column.$el;
  680. // });
  681. //// this breaks the event map
  682. // //this.$( '.middle' ).html( $els );
  683. //// this doesn't
  684. // this.$( '.middle' ).append( $els );
  685. var $middle = multipanel.$( '.middle' );
  686. $middle.empty();
  687. sortedAndFiltered.forEach( function( column, i ){
  688. //console.debug( 'rendering:', column, i, column.panel )
  689. column.$el.appendTo( $middle );
  690. column.delegateEvents();
  691. multipanel.renderColumn( column, speed );
  692. //column.$el.hide().appendTo( $middle );
  693. //multipanel.renderColumn( column, speed );
  694. // //.panel.on( 'all', function(){
  695. // // console.debug( 'column rendered:', arguments );
  696. // //});
  697. //// this won't work until we checkColumnsInView after the render
  698. ////column.$el.fadeIn( speed );
  699. //column.$el.show();
  700. });
  701. //this.log( 'column rendering done' );
  702. //TODO: event columns-rendered
  703. if( this.searchFor && sortedAndFiltered.length <= 1 ){
  704. } else {
  705. // check for in-view, hda lazy-loading if so
  706. multipanel.checkColumnsInView();
  707. // the first, current column has position: fixed and flex css will not apply - adjust height manually
  708. this._recalcFirstColumnHeight();
  709. }
  710. return sortedAndFiltered;
  711. },
  712. /** Render a single column using the non-blocking setTimeout( 0 ) pattern */
  713. renderColumn : function( column, speed ){
  714. speed = speed !== undefined? speed: this.fxSpeed;
  715. return column.render( speed );
  716. //TODO: causes weirdness
  717. //return _.delay( function(){
  718. // return column.render( speed );
  719. //}, 0 );
  720. },
  721. //TODO: combine the following two more sensibly
  722. //TODO: could have HistoryContents.haveDetails return false
  723. // if column.model.contents.length === 0 && !column.model.get( 'empty' ) then just check that
  724. /** Get the *summary* contents of a column's history (and details on any expanded contents),
  725. * queueing the ajax call and using a named queue to prevent the call being sent twice
  726. */
  727. queueHdaFetch : function queueHdaFetch( column ){
  728. //this.log( 'queueHdaFetch:', column );
  729. // if the history model says it has hdas but none are present, queue an ajax req for them
  730. if( column.model.contents.length === 0 && !column.model.get( 'empty' ) ){
  731. //this.log( '\t fetch needed:', column );
  732. var xhrData = {},
  733. ids = _.values( column.panel.storage.get( 'expandedIds' ) ).join();
  734. if( ids ){
  735. xhrData.dataset_details = ids;
  736. }
  737. // this uses a 'named' queue so that duplicate requests are ignored
  738. this.hdaQueue.add({
  739. name : column.model.id,
  740. fn : function(){
  741. var xhr = column.model.contents.fetch({ data: xhrData, silent: true });
  742. return xhr.done( function( response ){
  743. column.panel.renderItems();
  744. });
  745. }
  746. });
  747. // the queue is re-used, so if it's not processing requests - start it again
  748. if( !this.hdaQueue.running ){ this.hdaQueue.start(); }
  749. }
  750. },
  751. /** Get the *detailed* json for *all* of a column's history's contents - req'd for searching */
  752. queueHdaFetchDetails : function( column ){
  753. if( ( column.model.contents.length === 0 && !column.model.get( 'empty' ) )
  754. || ( !column.model.contents.haveDetails() ) ){
  755. // this uses a 'named' queue so that duplicate requests are ignored
  756. this.hdaQueue.add({
  757. name : column.model.id,
  758. fn : function(){
  759. var xhr = column.model.contents.fetch({ data: { details: 'all' }, silent: true });
  760. return xhr.done( function( response ){
  761. column.panel.renderItems();
  762. });
  763. }
  764. });
  765. // the queue is re-used, so if it's not processing requests - start it again
  766. if( !this.hdaQueue.running ){ this.hdaQueue.start(); }
  767. }
  768. },
  769. /** put a text msg in the header */
  770. renderInfo : function( msg ){
  771. this.$( '.header .header-info' ).text( msg );
  772. },
  773. // ------------------------------------------------------------------------ events/behaviors
  774. /** */
  775. events : {
  776. // will move to the server root (gen. Analyze data)
  777. 'click .done.btn' : 'close',
  778. //TODO:?? could just go back - but that's not always correct/desired behav.
  779. //'click .done.btn' : function(){ window.history.back(); },
  780. // creates a new empty history and makes it current
  781. 'click .create-new.btn' : 'create',
  782. // these change the collection and column sort order
  783. 'click .order .order-update' : function( e ){ this.setOrder( 'update' ); },
  784. 'click .order .order-name' : function( e ){ this.setOrder( 'name' ); },
  785. 'click .order .order-size' : function( e ){ this.setOrder( 'size' ); }
  786. //'dragstart .list-item .title-bar' : function( e ){ console.debug( 'ok' ); }
  787. },
  788. close : function( ev ){
  789. //TODO: switch to pushState/router
  790. var destination = '/';
  791. if( Galaxy && Galaxy.options && Galaxy.options.root ){
  792. destination = Galaxy.options.root;
  793. } else if( galaxy_config && galaxy_config.root ){
  794. destination = galaxy_config.root;
  795. }
  796. window.location = destination;
  797. },
  798. /** Include deleted histories in the collection */
  799. includeDeletedHistories : function(){
  800. //TODO: better through API/limit+offset
  801. window.location += ( /\?/.test( window.location.toString() ) )?( '&' ):( '?' )
  802. + 'include_deleted_histories=True';
  803. },
  804. /** Show only non-deleted histories */
  805. excludeDeletedHistories : function(){
  806. //TODO: better through API/limit+offset
  807. window.location = window.location.toString().replace( /[&\?]include_deleted_histories=True/g, '' );
  808. },
  809. /** Set up any view plugins */
  810. setUpBehaviors : function(){
  811. var multipanel = this;
  812. //TODO: currently doesn't need to be a mode button
  813. // toggle button for include deleted
  814. multipanel.$( '.include-deleted' ).modeButton({
  815. initialMode : this.collection.includeDeleted? 'exclude' : 'include',
  816. switchModesOnClick : false,
  817. modes: [
  818. { mode: 'include', html: _l( 'Include deleted histories' ),
  819. onclick: _.bind( multipanel.includeDeletedHistories, multipanel )
  820. },
  821. { mode: 'exclude', html: _l( 'Exclude deleted histories' ),
  822. onclick: _.bind( multipanel.excludeDeletedHistories, multipanel )
  823. }
  824. ]
  825. });
  826. // input to search histories
  827. multipanel.$( '#search-histories' ).searchInput({
  828. name : 'search-histories',
  829. placeholder : _l( 'search histories' ),
  830. onsearch : function( searchFor ){
  831. multipanel.searchFor = searchFor;
  832. multipanel.filters = [ function(){
  833. return this.model.matchesAll( multipanel.searchFor );
  834. }];
  835. multipanel.renderColumns( 0 );
  836. },
  837. onclear : function( searchFor ){
  838. multipanel.searchFor = null;
  839. //TODO: remove specifically not just reset
  840. multipanel.filters = [];
  841. multipanel.renderColumns( 0 );
  842. }
  843. });
  844. // input to search datasets
  845. multipanel.$( '#search-datasets' ).searchInput({
  846. name : 'search-datasets',
  847. placeholder : _l( 'search all datasets' ),
  848. onfirstsearch : function( searchFor ){
  849. multipanel.hdaQueue.clear();
  850. multipanel.$( '#search-datasets' ).searchInput( 'toggle-loading' );
  851. multipanel.searchFor = searchFor;
  852. multipanel.sortedFilteredColumns().forEach( function( column ){
  853. column.panel.searchItems( searchFor );
  854. // load details for them that need
  855. multipanel.queueHdaFetchDetails( column );
  856. });
  857. multipanel.hdaQueue.progress( function( progress ){
  858. multipanel.renderInfo([
  859. _l( 'searching' ), ( progress.curr + 1 ), _l( 'of' ), progress.total
  860. ].join( ' ' ));
  861. });
  862. multipanel.hdaQueue.deferred.done( function(){
  863. multipanel.renderInfo( '' );
  864. multipanel.$( '#search-datasets' ).searchInput( 'toggle-loading' );
  865. });
  866. },
  867. onsearch : function( searchFor ){
  868. multipanel.searchFor = searchFor;
  869. multipanel.sortedFilteredColumns().forEach( function( column ){
  870. column.panel.searchItems( searchFor );
  871. });
  872. },
  873. onclear : function( searchFor ){
  874. multipanel.searchFor = null;
  875. multipanel.sortedFilteredColumns().forEach( function( column ){
  876. column.panel.clearSearch();
  877. });
  878. }
  879. });
  880. //TODO: each panel stores the hidden/deleted state - and that isn't reflected in the buttons
  881. // toggle button for showing deleted history contents
  882. multipanel.$( '#toggle-deleted' ).modeButton({
  883. initialMode : 'include',
  884. modes: [
  885. { mode: 'exclude', html: _l( 'Exclude deleted datasets' ) },
  886. { mode: 'include', html: _l( 'Include deleted datasets' ) }
  887. ]
  888. }).click( function(){
  889. var show = $( this ).modeButton( 'getMode' ).mode === 'exclude';
  890. multipanel.sortedFilteredColumns().forEach( function( column, i ){
  891. _.delay( function(){
  892. column.panel.toggleShowDeleted( show, false );
  893. }, i * 200 );
  894. });
  895. });
  896. // toggle button for showing hidden history contents
  897. multipanel.$( '#toggle-hidden' ).modeButton({
  898. initialMode : 'include',
  899. modes: [
  900. { mode: 'exclude', html: _l( 'Exclude hidden datasets' ) },
  901. { mode: 'include', html: _l( 'Include hidden datasets' ) }
  902. ]
  903. }).click( function(){
  904. var show = $( this ).modeButton( 'getMode' ).mode === 'exclude';
  905. multipanel.sortedFilteredColumns().forEach( function( column, i ){
  906. _.delay( function(){
  907. column.panel.toggleShowHidden( show, false );
  908. }, i * 200 );
  909. });
  910. });
  911. // resize first (fixed position) column on page resize
  912. $( window ).resize( function(){
  913. multipanel._recalcFirstColumnHeight();
  914. });
  915. // when scrolling - check for histories now in view: they will fire 'in-view' and queueHdaLoading if necc.
  916. //TODO:?? might be able to simplify and not use pub-sub
  917. var debouncedInView = _.debounce( _.bind( this.checkColumnsInView, this ), 100 );
  918. this.$( '.middle' ).parent().scroll( debouncedInView );
  919. },
  920. ///** Put the in-view columns then the other columns in a queue, rendering each one at a time */
  921. //panelRenderQueue : function( columns, fn, args, renderEventName ){
  922. //},
  923. /** Adjust the height of the first, current column since flex-boxes won't work with fixed postiion elements */
  924. _recalcFirstColumnHeight : function(){
  925. var $firstColumn = this.$( '.history-column' ).first(),
  926. middleHeight = this.$( '.middle' ).height(),
  927. controlHeight = $firstColumn.find( '.panel-controls' ).height();
  928. $firstColumn.height( middleHeight )
  929. .find( '.inner' ).height( middleHeight - controlHeight );
  930. },
  931. /** Get the left and right pixel coords of the middle element */
  932. _viewport : function(){
  933. var viewLeft = this.$( '.middle' ).parent().offset().left;
  934. return { left: viewLeft, right: viewLeft + this.$( '.middle' ).parent().width() };
  935. },
  936. /** returns the columns currently in the viewport */
  937. columnsInView : function(){
  938. //TODO: uses offset which is render intensive
  939. //TODO: 2N - could use arg filter (sortedFilteredColumns( filter )) instead
  940. var vp = this._viewport();
  941. return this.sortedFilteredColumns().filter( function( column ){
  942. return column.currentHistory || column.inView( vp.left, vp.right );
  943. });
  944. },
  945. //TODO: sortByInView - return cols in view, then others
  946. /** trigger in-view from columns in-view */
  947. checkColumnsInView : function(){
  948. //TODO: assbackward
  949. //console.debug( 'checking columns in view', this.columnsInView() );
  950. this.columnsInView().forEach( function( column ){
  951. column.trigger( 'in-view', column );
  952. });
  953. },
  954. /** Show and enable the current columns drop target */
  955. currentColumnDropTargetOn : function(){
  956. var currentColumn = this.columnMap[ this.currentHistoryId ];
  957. if( !currentColumn ){ return; }
  958. //TODO: fix this - shouldn't need monkeypatch
  959. currentColumn.panel.dataDropped = function( data ){};
  960. currentColumn.panel.dropTargetOn();
  961. },
  962. /** Hide and disable the current columns drop target */
  963. currentColumnDropTargetOff : function(){
  964. var currentColumn = this.columnMap[ this.currentHistoryId ];
  965. if( !currentColumn ){ return; }
  966. currentColumn.panel.dataDropped = HPANEL_EDIT.HistoryPanelEdit.prototype.dataDrop;
  967. currentColumn.panel.dropTargetOff();
  968. },
  969. // ------------------------------------------------------------------------ misc
  970. /** String rep */
  971. toString : function(){
  972. return 'MultiPanelColumns(' + ( this.columns? this.columns.length : 0 ) + ')';
  973. }
  974. });
  975. //==============================================================================
  976. return {
  977. MultiPanelColumns : MultiPanelColumns
  978. };
  979. });