PageRenderTime 77ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 1ms

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

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