commit/galaxy-central: 15 new changesets
15 new commits in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/24539736d933/ Changeset: 24539736d933 User: jmchilton Date: 2014-04-10 22:46:06 Summary: Workflow editor backbonification - add NodeView. Including unit tests. Affected #: 2 files diff -r 12e6db99d15193b700cbde74f31c71a2ef9bcc2d -r 24539736d9331e3586a2e9cc32ee31da43105900 static/scripts/galaxy.workflow_editor.canvas.js --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -68,6 +68,58 @@ } }); + +////////////// +// START VIEWS +////////////// + + + +var NodeView = Backbone.View.extend( { + initialize: function( options ){ + this.node = options.node; + this.output_width = Math.max(150, this.$el.width()); + this.tool_body = this.$el.find( ".toolFormBody" ); + this.tool_body.find( "div" ).remove(); + $("<div class='inputs'></div>").appendTo( this.tool_body ); + }, + + render : function() { + this.renderToolErrors(); + this.$el.css( "width", Math.min(250, Math.max(this.$el.width(), this.output_width ))); + }, + + renderToolErrors: function( ) { + if ( this.node.tool_errors ) { + this.$el.addClass( "tool-node-error" ); + } else { + this.$el.removeClass( "tool-node-error" ); + } + }, + + updateMaxWidth: function( newWidth ) { + this.output_width = Math.max( this.output_width, newWidth ); + }, + + addRule: function() { + this.tool_body.append( $( "<div class='rule'></div>" ) ); + }, + + addDataInput: function( inputView ) { + var ib = inputView.$el; + var terminalElement = inputView.terminalElement; + this.$( ".inputs" ).append( ib.prepend( terminalElement ) ); + } + +} ); + + + +//////////// +// END VIEWS +//////////// + + function Connector( handle1, handle2 ) { this.canvas = null; this.dragging = false; @@ -295,7 +347,6 @@ $(element).removeClass( "toolForm-active" ); }, init_field_data : function ( data ) { - var f = this.element; if ( data.type ) { this.type = data.type; } @@ -308,16 +359,12 @@ this.post_job_actions = data.post_job_actions ? data.post_job_actions : {}; this.workflow_outputs = data.workflow_outputs ? data.workflow_outputs : []; - if ( this.tool_errors ) { - f.addClass( "tool-node-error" ); - } else { - f.removeClass( "tool-node-error" ); - } var node = this; - var output_width = Math.max(150, f.width()); - var b = f.find( ".toolFormBody" ); - b.find( "div" ).remove(); - var ibox = $("<div class='inputs'></div>").appendTo( b ); + var nodeView = new NodeView({ + el: this.element[ 0 ], + node: node, + }); + $.each( data.data_inputs, function( i, input ) { var t = node.new_input_terminal( input ); var ib = $("<div class='form-row dataRow input-data-row' name='" + input.name + "'>" + input.label + "</div>" ); @@ -335,7 +382,7 @@ ibox.append( ib.prepend( t ) ); }); if ( ( data.data_inputs.length > 0 ) && ( data.data_outputs.length > 0 ) ) { - b.append( $( "<div class='rule'></div>" ) ); + nodeView.addRule(); } $.each( data.data_outputs, function( i, output ) { var t = $( "<div class='terminal output-terminal'></div>" ); @@ -391,15 +438,15 @@ top: -1000, display:'none'}); $('body').append(r); - output_width = Math.max(output_width, r.outerWidth() + 17); + nodeView.updateMaxWidth( r.outerWidth() + 17 ); r.css({ position:'', left:'', top:'', display:'' }); r.detach(); - b.append( r.append( t ) ); + nodeView.tool_body.append( r.append( t ) ); }); - f.css( "width", Math.min(250, Math.max(f.width(), output_width ))); + nodeView.render(); workflow.node_changed( this ); }, update_field_data : function( data ) { diff -r 12e6db99d15193b700cbde74f31c71a2ef9bcc2d -r 24539736d9331e3586a2e9cc32ee31da43105900 test/qunit/tests/workflow_editor_tests.js --- a/test/qunit/tests/workflow_editor_tests.js +++ b/test/qunit/tests/workflow_editor_tests.js @@ -376,4 +376,41 @@ } ); } ); + /* global NodeView */ + module( "Node view ", { + setup: function() { + this.set_for_node( {} ); + }, + set_for_node: function( node ) { + var element = $("<div>"); + this.view = new NodeView( { node: node, el: element[ 0 ] } ); + }, + } ); + + test( "tool error styling", function() { + this.set_for_node( { tool_errors: false } ); + this.view.render(); + ok( ! this.view.$el.hasClass( "tool-node-error" ) ); + this.set_for_node( { tool_errors: true } ); + this.view.render(); + ok( this.view.$el.hasClass( "tool-node-error" ) ); + } ); + + test( "rendering correct width", function() { + // Default width is 150 + this.view.render(); + equal( this.view.$el.width(), 150 ); + + // If any data rows are greater, it will update + this.view.updateMaxWidth( 200 ); + this.view.render(); + equal( this.view.$el.width(), 200 ); + + // However 250 is the maximum width of node + this.view.updateMaxWidth( 300 ); + this.view.render(); + equal( this.view.$el.width(), 250 ); + + } ); + }); \ No newline at end of file https://bitbucket.org/galaxy/galaxy-central/commits/d43d2a1c14cb/ Changeset: d43d2a1c14cb User: jmchilton Date: 2014-04-10 22:46:06 Summary: Workflow editor backbonification - add DataInputView. Affected #: 1 file diff -r 24539736d9331e3586a2e9cc32ee31da43105900 -r d43d2a1c14cb8407c10a67af8400c4bf9a3236b8 static/scripts/galaxy.workflow_editor.canvas.js --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -115,6 +115,33 @@ +var DataInputView = Backbone.View.extend( { + className: "form-row dataRow input-data-row", + + initialize: function( options ){ + this.input = options.input; + this.nodeView = options.nodeView; + this.terminalElement = options.terminalElement; + + this.$el.attr( "name", this.input.name ) + .html( this.input.label ) + .css({ position:'absolute', + left: -1000, + top: -1000, + display:'none'}); + + $('body').append(this.el); + this.nodeView.updateMaxWidth( this.$el.outerWidth() ); + this.$el.css({ position:'', + left:'', + top:'', + display:'' }); + this.$el.remove(); + }, + +} ); + + //////////// // END VIEWS //////////// @@ -366,20 +393,12 @@ }); $.each( data.data_inputs, function( i, input ) { - var t = node.new_input_terminal( input ); - var ib = $("<div class='form-row dataRow input-data-row' name='" + input.name + "'>" + input.label + "</div>" ); - ib.css({ position:'absolute', - left: -1000, - top: -1000, - display:'none'}); - $('body').append(ib); - output_width = Math.max(output_width, ib.outerWidth()); - ib.css({ position:'', - left:'', - top:'', - display:'' }); - ib.remove(); - ibox.append( ib.prepend( t ) ); + var terminalElement = node.new_input_terminal( input ); + nodeView.addDataInput( new DataInputView( { + "terminalElement": terminalElement, + "input": input, + "nodeView": nodeView + } ) ); }); if ( ( data.data_inputs.length > 0 ) && ( data.data_outputs.length > 0 ) ) { nodeView.addRule(); https://bitbucket.org/galaxy/galaxy-central/commits/532e7490c35e/ Changeset: 532e7490c35e User: jmchilton Date: 2014-04-10 22:46:06 Summary: Workflow editor backbonification - add view for output callout. Affected #: 1 file diff -r d43d2a1c14cb8407c10a67af8400c4bf9a3236b8 -r 532e7490c35e6426572f38b5e288f1165baa4c1d static/scripts/galaxy.workflow_editor.canvas.js --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -142,6 +142,60 @@ } ); + +var OutputCalloutView = Backbone.View.extend( { + tagName: "div", + + initialize: function( options ) { + this.label = options.label; + this.node = options.node; + this.output = options.output; + + var view = this; + this.$el + .attr( "class", 'callout '+this.label ) + .css( { display: 'none' } ) + .append( + $("<div class='buttons'></div>").append( + $("<img/>").attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png').click( function() { + if ($.inArray(view.output.name, view.node.workflow_outputs) != -1){ + view.node.workflow_outputs.splice($.inArray(view.output.name, view.node.workflow_outputs), 1); + view.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png'); + }else{ + view.node.workflow_outputs.push(view.output.name); + view.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png'); + } + workflow.has_changes = true; + canvas_manager.draw_overview(); + }))) + .tooltip({delay:500, title: "Mark dataset as a workflow output. All unmarked datasets will be hidden." }); + + this.$el.css({ + top: '50%', + margin:'-8px 0px 0px 0px', + right: 8 + }); + this.$el.show(); + this.resetImage(); + }, + + resetImage: function() { + if ($.inArray( this.output.name, this.node.workflow_outputs) === -1){ + this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png'); + } else{ + this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png'); + } + }, + + hoverImage: function() { + this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-yellow.png'); + } + +} ); + + + + //////////// // END VIEWS //////////// @@ -412,45 +466,13 @@ } var r = $("<div class='form-row dataRow'>" + label + "</div>" ); if (node.type == 'tool'){ - var callout = $("<div class='callout "+label+"'></div>") - .css( { display: 'none' } ) - .append( - $("<div class='buttons'></div>").append( - $("<img/>").attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png').click( function() { - if ($.inArray(output.name, node.workflow_outputs) != -1){ - node.workflow_outputs.splice($.inArray(output.name, node.workflow_outputs), 1); - callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png'); - }else{ - node.workflow_outputs.push(output.name); - callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png'); - } - workflow.has_changes = true; - canvas_manager.draw_overview(); - }))) - .tooltip({delay:500, title: "Mark dataset as a workflow output. All unmarked datasets will be hidden." }); - callout.css({ - top: '50%', - margin:'-8px 0px 0px 0px', - right: 8 - }); - callout.show(); - r.append(callout); - if ($.inArray(output.name, node.workflow_outputs) === -1){ - callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png'); - }else{ - callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png'); - } - r.hover( - function(){ - callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-yellow.png'); - }, - function(){ - if ($.inArray(output.name, node.workflow_outputs) === -1){ - callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png'); - }else{ - callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png'); - } - }); + var calloutView = new OutputCalloutView( { + "label": label, + "output": output, + "node": node, + }); + r.append( calloutView.el ); + r.hover( function() { calloutView.hoverImage() }, function() { calloutView.resetImage() } ); } r.css({ position:'absolute', left: -1000, https://bitbucket.org/galaxy/galaxy-central/commits/6a62178693eb/ Changeset: 6a62178693eb User: jmchilton Date: 2014-04-10 22:46:06 Summary: Workflow editor backbonification - add view for output data view. Affected #: 1 file diff -r 532e7490c35e6426572f38b5e288f1165baa4c1d -r 6a62178693eb664344262fd12e147fb5696a185e static/scripts/galaxy.workflow_editor.canvas.js --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -109,6 +109,10 @@ var ib = inputView.$el; var terminalElement = inputView.terminalElement; this.$( ".inputs" ).append( ib.prepend( terminalElement ) ); + }, + + addDataOutput: function( outputView ) { + this.tool_body.append( outputView.$el.append( outputView.terminalElement ) ); } } ); @@ -196,6 +200,48 @@ +var DataOutputView = Backbone.View.extend( { + className: "form-row dataRow", + + initialize: function( options ) { + this.output = options.output; + this.terminalElement = options.terminalElement; + this.nodeView = options.nodeView; + + var output = this.output; + var label = output.name; + var node = this.nodeView.node; + if ( output.extensions.indexOf( 'input' ) < 0 ) { + label = label + " (" + output.extensions.join(", ") + ")"; + } + this.$el.html( label ) + + if (node.type == 'tool'){ + var calloutView = new OutputCalloutView( { + "label": label, + "output": output, + "node": node, + }); + this.$el.append( calloutView.el ); + this.$el.hover( function() { calloutView.hoverImage() }, function() { calloutView.resetImage() } ); + } + this.$el.css({ position:'absolute', + left: -1000, + top: -1000, + display:'none'}); + $('body').append( this.el ); + this.nodeView.updateMaxWidth( this.$el.outerWidth() + 17 ); + this.$el.css({ position:'', + left:'', + top:'', + display:'' }) + .detach(); + } + +} ); + + + //////////// // END VIEWS //////////// @@ -458,34 +504,13 @@ nodeView.addRule(); } $.each( data.data_outputs, function( i, output ) { - var t = $( "<div class='terminal output-terminal'></div>" ); - node.enable_output_terminal( t[ 0 ], output.name, output.extensions ); - var label = output.name; - if ( output.extensions.indexOf( 'input' ) < 0 ) { - label = label + " (" + output.extensions.join(", ") + ")"; - } - var r = $("<div class='form-row dataRow'>" + label + "</div>" ); - if (node.type == 'tool'){ - var calloutView = new OutputCalloutView( { - "label": label, - "output": output, - "node": node, - }); - r.append( calloutView.el ); - r.hover( function() { calloutView.hoverImage() }, function() { calloutView.resetImage() } ); - } - r.css({ position:'absolute', - left: -1000, - top: -1000, - display:'none'}); - $('body').append(r); - nodeView.updateMaxWidth( r.outerWidth() + 17 ); - r.css({ position:'', - left:'', - top:'', - display:'' }); - r.detach(); - nodeView.tool_body.append( r.append( t ) ); + var terminalElement = $( "<div class='terminal output-terminal'></div>" ); + node.enable_output_terminal( terminalElement[ 0 ], output.name, output.extensions ); + nodeView.addDataOutput( new DataOutputView( { + "output": output, + "terminalElement": terminalElement, + "nodeView": nodeView, + } ) ); }); nodeView.render(); workflow.node_changed( this ); https://bitbucket.org/galaxy/galaxy-central/commits/00cfe74bb2af/ Changeset: 00cfe74bb2af User: jmchilton Date: 2014-04-10 22:46:06 Summary: Workflow editor backbonification - add InputTerminalView. Add unit tests for new view. Affected #: 2 files diff -r 6a62178693eb664344262fd12e147fb5696a185e -r 00cfe74bb2afee051f62859fe9ae4b49327ec097 static/scripts/galaxy.workflow_editor.canvas.js --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -242,6 +242,72 @@ +var InputTerminalView = Backbone.View.extend( { + className: "terminal input-terminal", + + initialize: function( options ) { + var node = options.node; + var input = options.input; + + var name = input.name; + var types = input.extensions; + var multiple = input.multiple; + + var element = this.el; + + var terminal = element.terminal = new InputTerminal( element, types, multiple ); + terminal.node = node; + terminal.name = name; + $(element).bind( "dropinit", function( e, d ) { + // Accept a dragable if it is an output terminal and has a + // compatible type + return $(d.drag).hasClass( "output-terminal" ) && terminal.can_accept( d.drag.terminal ); + }).bind( "dropstart", function( e, d ) { + if (d.proxy.terminal) { + d.proxy.terminal.connectors[0].inner_color = "#BBFFBB"; + } + }).bind( "dropend", function ( e, d ) { + if (d.proxy.terminal) { + d.proxy.terminal.connectors[0].inner_color = "#FFFFFF"; + } + }).bind( "drop", function( e, d ) { + ( new Connector( d.drag.terminal, terminal ) ).redraw(); + }).bind( "hover", function() { + // If connected, create a popup to allow disconnection + if ( terminal.connectors.length > 0 ) { + // Create callout + var t = $("<div class='callout'></div>") + .css( { display: 'none' } ) + .appendTo( "body" ) + .append( + $("<div class='button'></div>").append( + $("<div/>").addClass("fa-icon-button fa fa-times").click( function() { + $.each( terminal.connectors, function( _, x ) { + if (x) { + x.destroy(); + } + }); + t.remove(); + }))) + .bind( "mouseleave", function() { + $(this).remove(); + }); + // Position it and show + t.css({ + top: $(element).offset().top - 2, + left: $(element).offset().left - t.width(), + 'padding-right': $(element).width() + }).show(); + } + }); + + node.input_terminals[name] = terminal; + }, +} ); + + + + //////////// // END VIEWS //////////// @@ -350,61 +416,6 @@ this.tool_errors = {}; } $.extend( Node.prototype, { - new_input_terminal : function( input ) { - var t = $("<div class='terminal input-terminal'></div>")[ 0 ]; - this.enable_input_terminal( t, input.name, input.extensions, input.multiple ); - return t; - }, - enable_input_terminal : function( element, name, types, multiple ) { - var node = this; - - var terminal = element.terminal = new InputTerminal( element, types, multiple ); - terminal.node = node; - terminal.name = name; - $(element).bind( "dropinit", function( e, d ) { - // Accept a dragable if it is an output terminal and has a - // compatible type - return $(d.drag).hasClass( "output-terminal" ) && terminal.can_accept( d.drag.terminal ); - }).bind( "dropstart", function( e, d ) { - if (d.proxy.terminal) { - d.proxy.terminal.connectors[0].inner_color = "#BBFFBB"; - } - }).bind( "dropend", function ( e, d ) { - if (d.proxy.terminal) { - d.proxy.terminal.connectors[0].inner_color = "#FFFFFF"; - } - }).bind( "drop", function( e, d ) { - ( new Connector( d.drag.terminal, terminal ) ).redraw(); - }).bind( "hover", function() { - // If connected, create a popup to allow disconnection - if ( terminal.connectors.length > 0 ) { - // Create callout - var t = $("<div class='callout'></div>") - .css( { display: 'none' } ) - .appendTo( "body" ) - .append( - $("<div class='button'></div>").append( - $("<div/>").addClass("fa-icon-button fa fa-times").click( function() { - $.each( terminal.connectors, function( _, x ) { - if (x) { - x.destroy(); - } - }); - t.remove(); - }))) - .bind( "mouseleave", function() { - $(this).remove(); - }); - // Position it and show - t.css({ - top: $(element).offset().top - 2, - left: $(element).offset().left - t.width(), - 'padding-right': $(element).width() - }).show(); - } - }); - node.input_terminals[name] = terminal; - }, enable_output_terminal : function( element, name, type ) { var node = this; var terminal_element = element; @@ -493,7 +504,11 @@ }); $.each( data.data_inputs, function( i, input ) { - var terminalElement = node.new_input_terminal( input ); + var terminalView = new InputTerminalView( { + node: node, + input: input + } ); + var terminalElement = terminalView.el; nodeView.addDataInput( new DataInputView( { "terminalElement": terminalElement, "input": input, @@ -533,7 +548,12 @@ var old_body = el.find( "div.inputs" ); var new_body = $("<div class='inputs'></div>"); $.each( data.data_inputs, function( i, input ) { - var t = node.new_input_terminal( input ); + var terminalView = new InputTerminalView( { + node: node, + input: input + } ); + var t = terminalView.el; + // If already connected save old connection old_body.find( "div[name='" + input.name + "']" ).each( function() { $(this).find( ".input-terminal" ).each( function() { diff -r 6a62178693eb664344262fd12e147fb5696a185e -r 00cfe74bb2afee051f62859fe9ae4b49327ec097 test/qunit/tests/workflow_editor_tests.js --- a/test/qunit/tests/workflow_editor_tests.js +++ b/test/qunit/tests/workflow_editor_tests.js @@ -413,4 +413,30 @@ } ); + /* global InputTerminalView */ + module( "Input terminal view", { + setup: function() { + this.node = { input_terminals: [] }; + this.input = { name: "i1", extensions: "txt", multiple: false }; + this.view = new InputTerminalView( { + node: this.node, + input: this.input, + }); + } + } ); + + test( "terminal added to node", function() { + ok( this.node.input_terminals.i1 ); + equal( this.node.input_terminals.i1.datatypes, [ "txt" ] ); + equal( this.node.input_terminals.i1.multiple, false ); + } ); + + test( "terminal element", function() { + var el = this.view.el; + equal( el.tagName, "DIV" ); + equal( el.className, "terminal input-terminal"); + } ); + + // TODO: Test binding... not sure how to do that exactly.. + }); \ No newline at end of file https://bitbucket.org/galaxy/galaxy-central/commits/4ec192c78091/ Changeset: 4ec192c78091 User: jmchilton Date: 2014-04-10 22:46:07 Summary: Workflow editor backbonification - add OutputTerminalView. Add unit tests for OutputTerminalView. Affected #: 2 files diff -r 00cfe74bb2afee051f62859fe9ae4b49327ec097 -r 4ec192c78091ea3de586de05f2f0d9ccc1ec6c49 static/scripts/galaxy.workflow_editor.canvas.js --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -307,6 +307,57 @@ +var OutputTerminalView = Backbone.View.extend( { + className: "terminal output-terminal", + + initialize: function( options ) { + var node = options.node; + var output = options.output; + var name = output.name; + var type = output.extensions; + + var element = this.el; + var terminal_element = element; + var terminal = element.terminal = new OutputTerminal( element, type ); + terminal.node = node; + terminal.name = name; + $(element).bind( "dragstart", function( e, d ) { + $( d.available ).addClass( "input-terminal-active" ); + // Save PJAs in the case of change datatype actions. + workflow.check_changes_in_active_form(); + // Drag proxy div + var h = $( '<div class="drag-terminal" style="position: absolute;"></div>' ) + .appendTo( "#canvas-container" ).get(0); + // Terminal and connection to display noodle while dragging + h.terminal = new OutputTerminal( h ); + var c = new Connector(); + c.dragging = true; + c.connect( element.terminal, h.terminal ); + return h; + }).bind( "drag", function ( e, d ) { + var onmove = function() { + var po = $(d.proxy).offsetParent().offset(), + x = d.offsetX - po.left, + y = d.offsetY - po.top; + $(d.proxy).css( { left: x, top: y } ); + d.proxy.terminal.redraw(); + // FIXME: global + canvas_manager.update_viewport_overlay(); + }; + onmove(); + $("#canvas-container").get(0).scroll_panel.test( e, onmove ); + }).bind( "dragend", function ( e, d ) { + d.proxy.terminal.connectors[0].destroy(); + $(d.proxy).remove(); + $( d.available ).removeClass( "input-terminal-active" ); + $("#canvas-container").get(0).scroll_panel.stop(); + }); + node.output_terminals[name] = terminal; + } + +} ); + + //////////// // END VIEWS @@ -416,45 +467,6 @@ this.tool_errors = {}; } $.extend( Node.prototype, { - enable_output_terminal : function( element, name, type ) { - var node = this; - var terminal_element = element; - var terminal = element.terminal = new OutputTerminal( element, type ); - terminal.node = node; - terminal.name = name; - $(element).bind( "dragstart", function( e, d ) { - $( d.available ).addClass( "input-terminal-active" ); - // Save PJAs in the case of change datatype actions. - workflow.check_changes_in_active_form(); - // Drag proxy div - var h = $( '<div class="drag-terminal" style="position: absolute;"></div>' ) - .appendTo( "#canvas-container" ).get(0); - // Terminal and connection to display noodle while dragging - h.terminal = new OutputTerminal( h ); - var c = new Connector(); - c.dragging = true; - c.connect( element.terminal, h.terminal ); - return h; - }).bind( "drag", function ( e, d ) { - var onmove = function() { - var po = $(d.proxy).offsetParent().offset(), - x = d.offsetX - po.left, - y = d.offsetY - po.top; - $(d.proxy).css( { left: x, top: y } ); - d.proxy.terminal.redraw(); - // FIXME: global - canvas_manager.update_viewport_overlay(); - }; - onmove(); - $("#canvas-container").get(0).scroll_panel.test( e, onmove ); - }).bind( "dragend", function ( e, d ) { - d.proxy.terminal.connectors[0].destroy(); - $(d.proxy).remove(); - $( d.available ).removeClass( "input-terminal-active" ); - $("#canvas-container").get(0).scroll_panel.stop(); - }); - node.output_terminals[name] = terminal; - }, redraw : function () { $.each( this.input_terminals, function( _, t ) { t.redraw(); @@ -519,11 +531,13 @@ nodeView.addRule(); } $.each( data.data_outputs, function( i, output ) { - var terminalElement = $( "<div class='terminal output-terminal'></div>" ); - node.enable_output_terminal( terminalElement[ 0 ], output.name, output.extensions ); + var terminalView = new OutputTerminalView( { + node: node, + output: output + } ); nodeView.addDataOutput( new DataOutputView( { "output": output, - "terminalElement": terminalElement, + "terminalElement": terminalView.el, "nodeView": nodeView, } ) ); }); diff -r 00cfe74bb2afee051f62859fe9ae4b49327ec097 -r 4ec192c78091ea3de586de05f2f0d9ccc1ec6c49 test/qunit/tests/workflow_editor_tests.js --- a/test/qunit/tests/workflow_editor_tests.js +++ b/test/qunit/tests/workflow_editor_tests.js @@ -439,4 +439,29 @@ // TODO: Test binding... not sure how to do that exactly.. + /* global OutputTerminalView */ + module( "Output terminal view", { + setup: function() { + this.node = { output_terminals: [] }; + this.output = { name: "o1", extensions: "txt" }; + this.view = new OutputTerminalView( { + node: this.node, + output: this.output, + }); + } + } ); + + test( "terminal added to node", function() { + ok( this.node.output_terminals.o1 ); + equal( this.node.output_terminals.o1.datatypes, [ "txt" ] ); + } ); + + test( "terminal element", function() { + var el = this.view.el; + equal( el.tagName, "DIV" ); + equal( el.className, "terminal output-terminal"); + } ); + + // TODO: Test bindings + }); \ No newline at end of file https://bitbucket.org/galaxy/galaxy-central/commits/0274e805ac70/ Changeset: 0274e805ac70 User: jmchilton Date: 2014-04-10 22:46:07 Summary: Workflow editor backbonification - InputTerminalView to Backbone.View event binding. Affected #: 1 file diff -r 4ec192c78091ea3de586de05f2f0d9ccc1ec6c49 -r 0274e805ac700ae255c0ba8aa26a18ff48da08bd static/scripts/galaxy.workflow_editor.canvas.js --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -253,56 +253,77 @@ var types = input.extensions; var multiple = input.multiple; - var element = this.el; - - var terminal = element.terminal = new InputTerminal( element, types, multiple ); + var terminal = this.el.terminal = new InputTerminal( this.el, types, multiple ); terminal.node = node; terminal.name = name; - $(element).bind( "dropinit", function( e, d ) { - // Accept a dragable if it is an output terminal and has a - // compatible type - return $(d.drag).hasClass( "output-terminal" ) && terminal.can_accept( d.drag.terminal ); - }).bind( "dropstart", function( e, d ) { - if (d.proxy.terminal) { - d.proxy.terminal.connectors[0].inner_color = "#BBFFBB"; - } - }).bind( "dropend", function ( e, d ) { - if (d.proxy.terminal) { - d.proxy.terminal.connectors[0].inner_color = "#FFFFFF"; - } - }).bind( "drop", function( e, d ) { - ( new Connector( d.drag.terminal, terminal ) ).redraw(); - }).bind( "hover", function() { - // If connected, create a popup to allow disconnection - if ( terminal.connectors.length > 0 ) { - // Create callout - var t = $("<div class='callout'></div>") - .css( { display: 'none' } ) - .appendTo( "body" ) - .append( - $("<div class='button'></div>").append( - $("<div/>").addClass("fa-icon-button fa fa-times").click( function() { - $.each( terminal.connectors, function( _, x ) { - if (x) { - x.destroy(); - } - }); - t.remove(); - }))) - .bind( "mouseleave", function() { - $(this).remove(); - }); - // Position it and show - t.css({ - top: $(element).offset().top - 2, - left: $(element).offset().left - t.width(), - 'padding-right': $(element).width() - }).show(); - } - }); node.input_terminals[name] = terminal; }, + + events: { + "dropinit": "onDropInit", + "dropstart": "onDropStart", + "dropend": "onDropEnd", + "drop": "onDrop", + "hover": "onHover", + }, + + onDropInit: function( e, d ) { + var terminal = this.el.terminal; + // Accept a dragable if it is an output terminal and has a + // compatible type + return $(d.drag).hasClass( "output-terminal" ) && terminal.can_accept( d.drag.terminal ); + }, + + onDropStart: function( e, d ) { + if (d.proxy.terminal) { + d.proxy.terminal.connectors[0].inner_color = "#BBFFBB"; + } + }, + + onDropEnd: function ( e, d ) { + if (d.proxy.terminal) { + d.proxy.terminal.connectors[0].inner_color = "#FFFFFF"; + } + }, + + onDrop: function( e, d ) { + var terminal = this.el.terminal; + new Connector( d.drag.terminal, terminal ).redraw(); + }, + + onHover: function() { + var element = this.el; + var terminal = element.terminal; + + // If connected, create a popup to allow disconnection + if ( terminal.connectors.length > 0 ) { + // Create callout + var t = $("<div class='callout'></div>") + .css( { display: 'none' } ) + .appendTo( "body" ) + .append( + $("<div class='button'></div>").append( + $("<div/>").addClass("fa-icon-button fa fa-times").click( function() { + $.each( terminal.connectors, function( _, x ) { + if (x) { + x.destroy(); + } + }); + t.remove(); + }))) + .bind( "mouseleave", function() { + $(this).remove(); + }); + // Position it and show + t.css({ + top: $(element).offset().top - 2, + left: $(element).offset().left - t.width(), + 'padding-right': $(element).width() + }).show(); + } + }, + } ); https://bitbucket.org/galaxy/galaxy-central/commits/63f7752b59fd/ Changeset: 63f7752b59fd User: jmchilton Date: 2014-04-10 22:46:07 Summary: Workflow editor backbonification - OutputTerminalView to Backbone.View event binding. Affected #: 1 file diff -r 0274e805ac700ae255c0ba8aa26a18ff48da08bd -r 63f7752b59fdc717330e4e9988fb8da18dde90b0 static/scripts/galaxy.workflow_editor.canvas.js --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -342,38 +342,49 @@ var terminal = element.terminal = new OutputTerminal( element, type ); terminal.node = node; terminal.name = name; - $(element).bind( "dragstart", function( e, d ) { - $( d.available ).addClass( "input-terminal-active" ); - // Save PJAs in the case of change datatype actions. - workflow.check_changes_in_active_form(); - // Drag proxy div - var h = $( '<div class="drag-terminal" style="position: absolute;"></div>' ) - .appendTo( "#canvas-container" ).get(0); - // Terminal and connection to display noodle while dragging - h.terminal = new OutputTerminal( h ); - var c = new Connector(); - c.dragging = true; - c.connect( element.terminal, h.terminal ); - return h; - }).bind( "drag", function ( e, d ) { - var onmove = function() { - var po = $(d.proxy).offsetParent().offset(), - x = d.offsetX - po.left, - y = d.offsetY - po.top; - $(d.proxy).css( { left: x, top: y } ); - d.proxy.terminal.redraw(); - // FIXME: global - canvas_manager.update_viewport_overlay(); - }; - onmove(); - $("#canvas-container").get(0).scroll_panel.test( e, onmove ); - }).bind( "dragend", function ( e, d ) { - d.proxy.terminal.connectors[0].destroy(); - $(d.proxy).remove(); - $( d.available ).removeClass( "input-terminal-active" ); - $("#canvas-container").get(0).scroll_panel.stop(); - }); node.output_terminals[name] = terminal; + }, + + events: { + "drag": "onDrag", + "dragstart": "onDragStart", + "dragend": "onDragEnd", + }, + + onDrag: function ( e, d ) { + var onmove = function() { + var po = $(d.proxy).offsetParent().offset(), + x = d.offsetX - po.left, + y = d.offsetY - po.top; + $(d.proxy).css( { left: x, top: y } ); + d.proxy.terminal.redraw(); + // FIXME: global + canvas_manager.update_viewport_overlay(); + }; + onmove(); + $("#canvas-container").get(0).scroll_panel.test( e, onmove ); + }, + + onDragStart: function( e, d ) { + $( d.available ).addClass( "input-terminal-active" ); + // Save PJAs in the case of change datatype actions. + workflow.check_changes_in_active_form(); + // Drag proxy div + var h = $( '<div class="drag-terminal" style="position: absolute;"></div>' ) + .appendTo( "#canvas-container" ).get(0); + // Terminal and connection to display noodle while dragging + h.terminal = new OutputTerminal( h ); + var c = new Connector(); + c.dragging = true; + c.connect( this.el.terminal, h.terminal ); + return h; + }, + + onDragEnd: function ( e, d ) { + d.proxy.terminal.connectors[0].destroy(); + $(d.proxy).remove(); + $( d.available ).removeClass( "input-terminal-active" ); + $("#canvas-container").get(0).scroll_panel.stop(); } } ); https://bitbucket.org/galaxy/galaxy-central/commits/a0e1c04f77ec/ Changeset: a0e1c04f77ec User: jmchilton Date: 2014-04-10 22:46:07 Summary: Workflow editor backbonification - move terminal presentation logic from NodeView to DataInputView. Affected #: 1 file diff -r 63f7752b59fdc717330e4e9988fb8da18dde90b0 -r a0e1c04f77ec7d5727a01c7ee2a79b8a88121968 static/scripts/galaxy.workflow_editor.canvas.js --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -105,7 +105,17 @@ this.tool_body.append( $( "<div class='rule'></div>" ) ); }, - addDataInput: function( inputView ) { + addDataInput: function( input ) { + var terminalView = new InputTerminalView( { + node: this.node, + input: input + } ); + var terminalElement = terminalView.el; + var inputView = new DataInputView( { + "terminalElement": terminalElement, + "input": input, + "nodeView": this, + } ); var ib = inputView.$el; var terminalElement = inputView.terminalElement; this.$( ".inputs" ).append( ib.prepend( terminalElement ) ); @@ -548,16 +558,7 @@ }); $.each( data.data_inputs, function( i, input ) { - var terminalView = new InputTerminalView( { - node: node, - input: input - } ); - var terminalElement = terminalView.el; - nodeView.addDataInput( new DataInputView( { - "terminalElement": terminalElement, - "input": input, - "nodeView": nodeView - } ) ); + nodeView.addDataInput( input ); }); if ( ( data.data_inputs.length > 0 ) && ( data.data_outputs.length > 0 ) ) { nodeView.addRule(); https://bitbucket.org/galaxy/galaxy-central/commits/8facb774346b/ Changeset: 8facb774346b User: jmchilton Date: 2014-04-10 22:46:07 Summary: Workflow editor backbonification - move terminal presentation logic from NodeView to DataOutputView. Affected #: 1 file diff -r a0e1c04f77ec7d5727a01c7ee2a79b8a88121968 -r 8facb774346bb47abd7adb929394bf2b54eb5657 static/scripts/galaxy.workflow_editor.canvas.js --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -121,7 +121,16 @@ this.$( ".inputs" ).append( ib.prepend( terminalElement ) ); }, - addDataOutput: function( outputView ) { + addDataOutput: function( output ) { + var terminalView = new OutputTerminalView( { + node: this.node, + output: output + } ); + var outputView = new DataOutputView( { + "output": output, + "terminalElement": terminalView.el, + "nodeView": this, + } ); this.tool_body.append( outputView.$el.append( outputView.terminalElement ) ); } @@ -564,16 +573,8 @@ nodeView.addRule(); } $.each( data.data_outputs, function( i, output ) { - var terminalView = new OutputTerminalView( { - node: node, - output: output - } ); - nodeView.addDataOutput( new DataOutputView( { - "output": output, - "terminalElement": terminalView.el, - "nodeView": nodeView, - } ) ); - }); + nodeView.addDataOutput( output ); + } ); nodeView.render(); workflow.node_changed( this ); }, https://bitbucket.org/galaxy/galaxy-central/commits/118962ee000a/ Changeset: 118962ee000a User: jmchilton Date: 2014-04-10 22:46:07 Summary: Workflow editor backbonification - refactor some update_field_data presentation logic into NodeView. Affected #: 1 file diff -r 8facb774346bb47abd7adb929394bf2b54eb5657 -r 118962ee000a5337d2c1503c7bdb8d526ad7a7e7 static/scripts/galaxy.workflow_editor.canvas.js --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -81,7 +81,7 @@ this.output_width = Math.max(150, this.$el.width()); this.tool_body = this.$el.find( ".toolFormBody" ); this.tool_body.find( "div" ).remove(); - $("<div class='inputs'></div>").appendTo( this.tool_body ); + this.newInputsDiv().appendTo( this.tool_body ); }, render : function() { @@ -97,6 +97,10 @@ } }, + newInputsDiv: function() { + return $("<div class='inputs'></div>"); + }, + updateMaxWidth: function( newWidth ) { this.output_width = Math.max( this.output_width, newWidth ); }, @@ -121,6 +125,37 @@ this.$( ".inputs" ).append( ib.prepend( terminalElement ) ); }, + replaceDataInput: function( input, new_body ) { + var terminalView = new InputTerminalView( { + node: this.node, + input: input + } ); + var t = terminalView.el; + + // If already connected save old connection + this.$( "div[name='" + input.name + "']" ).each( function() { + $(this).find( ".input-terminal" ).each( function() { + var c = this.terminal.connectors[0]; + if ( c ) { + t.terminal.connectors[0] = c; + c.handle2 = t.terminal; + } + }); + $(this).remove(); + }); + var inputView = new DataInputView( { + "terminalElement": t, + "input": input, + "nodeView": this, + "skipResize": true, + } ); + var ib = inputView.$el; + + // Append to new body + new_body.append( ib.prepend( t ) ); + + }, + addDataOutput: function( output ) { var terminalView = new OutputTerminalView( { node: this.node, @@ -147,19 +182,21 @@ this.terminalElement = options.terminalElement; this.$el.attr( "name", this.input.name ) - .html( this.input.label ) - .css({ position:'absolute', - left: -1000, - top: -1000, - display:'none'}); - + .html( this.input.label ); + + if( ! options.skipResize ) { + this.$el.css({ position:'absolute', + left: -1000, + top: -1000, + display:'none'}); $('body').append(this.el); - this.nodeView.updateMaxWidth( this.$el.outerWidth() ); - this.$el.css({ position:'', - left:'', - top:'', - display:'' }); - this.$el.remove(); + this.nodeView.updateMaxWidth( this.$el.outerWidth() ); + this.$el.css({ position:'', + left:'', + top:'', + display:'' }); + this.$el.remove(); + } }, } ); @@ -565,6 +602,7 @@ el: this.element[ 0 ], node: node, }); + node.nodeView = nodeView; $.each( data.data_inputs, function( i, input ) { nodeView.addDataInput( input ); @@ -579,42 +617,20 @@ workflow.node_changed( this ); }, update_field_data : function( data ) { - var el = $(this.element), - node = this; + var node = this; + nodeView = node.nodeView; this.tool_state = data.tool_state; this.form_html = data.form_html; this.tool_errors = data.tool_errors; this.annotation = data['annotation']; var pja_in = $.parseJSON(data.post_job_actions); this.post_job_actions = pja_in ? pja_in : {}; - if ( this.tool_errors ) { - el.addClass( "tool-node-error" ); - } else { - el.removeClass( "tool-node-error" ); - } + node.nodeView.renderToolErrors(); // Update input rows - var old_body = el.find( "div.inputs" ); - var new_body = $("<div class='inputs'></div>"); + var old_body = nodeView.$( "div.inputs" ); + var new_body = nodeView.newInputsDiv(); $.each( data.data_inputs, function( i, input ) { - var terminalView = new InputTerminalView( { - node: node, - input: input - } ); - var t = terminalView.el; - - // If already connected save old connection - old_body.find( "div[name='" + input.name + "']" ).each( function() { - $(this).find( ".input-terminal" ).each( function() { - var c = this.terminal.connectors[0]; - if ( c ) { - t.terminal.connectors[0] = c; - c.handle2 = t.terminal; - } - }); - $(this).remove(); - }); - // Append to new body - new_body.append( $("<div class='form-row dataRow input-data-row' name='" + input.name + "'>" + input.label + "</div>" ).prepend( t ) ); + node.nodeView.replaceDataInput( input, new_body ); }); old_body.replaceWith( new_body ); // Cleanup any leftover terminals https://bitbucket.org/galaxy/galaxy-central/commits/61b3c6c4be0e/ Changeset: 61b3c6c4be0e User: jmchilton Date: 2014-04-10 22:46:07 Summary: Workflow editor backbonification - make Node a model. Affected #: 2 files diff -r 118962ee000a5337d2c1503c7bdb8d526ad7a7e7 -r 61b3c6c4be0e1c8d02ef9073d294b61db461f4b3 static/scripts/galaxy.workflow_editor.canvas.js --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -6,13 +6,13 @@ connect: function ( connector ) { this.connectors.push( connector ); if ( this.node ) { - this.node.changed(); + this.node.markChanged(); } }, disconnect: function ( connector ) { this.connectors.splice( $.inArray( connector, this.connectors ), 1 ); if ( this.node ) { - this.node.changed(); + this.node.markChanged(); } }, redraw: function () { @@ -548,13 +548,14 @@ } } ); -function Node( element ) { - this.element = element; - this.input_terminals = {}; - this.output_terminals = {}; - this.tool_errors = {}; -} -$.extend( Node.prototype, { +var Node = Backbone.Model.extend({ + + initialize: function( attr ) { + this.element = attr.element; + this.input_terminals = {}; + this.output_terminals = {}; + this.tool_errors = {}; + }, redraw : function () { $.each( this.input_terminals, function( _, t ) { t.redraw(); @@ -638,7 +639,7 @@ this.terminal.destroy(); }); // If active, reactivate with new form_html - this.changed(); + this.markChanged(); this.redraw(); }, error : function ( text ) { @@ -649,7 +650,7 @@ b.html( tmp ); workflow.node_changed( this ); }, - changed: function() { + markChanged: function() { workflow.node_changed( this ); } } ); @@ -1034,7 +1035,7 @@ function prebuild_node( type, title_text, tool_id ) { var f = $("<div class='toolForm toolFormInCanvas'></div>"); - var node = new Node( f ); + var node = new Node( { element: f } ); node.type = type; if ( type == 'tool' ) { node.tool_id = tool_id; diff -r 118962ee000a5337d2c1503c7bdb8d526ad7a7e7 -r 61b3c6c4be0e1c8d02ef9073d294b61db461f4b3 test/qunit/tests/workflow_editor_tests.js --- a/test/qunit/tests/workflow_editor_tests.js +++ b/test/qunit/tests/workflow_editor_tests.js @@ -81,25 +81,25 @@ } ); test( "test connect", function() { - this.node.changed = sinon.spy(); + this.node.markChanged = sinon.spy(); var connector = {}; this.input_terminal.connect( connector ); - // Assert node changed called - ok( this.node.changed.called ); + // Assert node markChanged called + ok( this.node.markChanged.called ); // Assert connectors updated ok( this.input_terminal.connectors[ 0 ] === connector ); } ); test( "test disconnect", function() { - this.node.changed = sinon.spy(); + this.node.markChanged = sinon.spy(); var connector = this.test_connector( {} ); this.input_terminal.disconnect( connector ); - // Assert node changed called - ok( this.node.changed.called ); + // Assert node markChanged called + ok( this.node.markChanged.called ); // Assert connectors updated equal( this.input_terminal.connectors.length, 0 ); } ); @@ -204,7 +204,7 @@ this.input_terminal = { destroy: sinon.spy(), redraw: sinon.spy() }; this.output_terminal = { destroy: sinon.spy(), redraw: sinon.spy() }; this.element = $("<div><div class='toolFormBody'></div></div>"); - this.node = new Node( this.element ); + this.node = new Node( { element: this.element } ); this.node.input_terminals.i1 = this.input_terminal; this.node.output_terminals.o1 = this.output_terminal; }, https://bitbucket.org/galaxy/galaxy-central/commits/4957c7b9a709/ Changeset: 4957c7b9a709 User: jmchilton Date: 2014-04-10 22:46:07 Summary: Workflow editor backbonification - make Terminal classes into models. Affected #: 2 files diff -r 61b3c6c4be0e1c8d02ef9073d294b61db461f4b3 -r 4957c7b9a70979fa10f85619ad608b071827df66 static/scripts/galaxy.workflow_editor.canvas.js --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -1,8 +1,8 @@ -function Terminal( element ) { - this.element = element; - this.connectors = []; -} -$.extend( Terminal.prototype, { +var Terminal = Backbone.Model.extend( { + initialize: function( attr ) { + this.element = attr.element; + this.connectors = []; + }, connect: function ( connector ) { this.connectors.push( connector ); if ( this.node ) { @@ -25,48 +25,14 @@ c.destroy(); }); } -}); +} ); -function OutputTerminal( element, datatypes ) { - Terminal.call( this, element ); - this.datatypes = datatypes; -} - -OutputTerminal.prototype = new Terminal(); - -function InputTerminal( element, datatypes, multiple ) { - Terminal.call( this, element ); - this.datatypes = datatypes; - this.multiple = multiple -} - -InputTerminal.prototype = new Terminal(); - -$.extend( InputTerminal.prototype, { - can_accept: function ( other ) { - if ( this.connectors.length < 1 || this.multiple) { - for ( var t in this.datatypes ) { - var cat_outputs = new Array(); - cat_outputs = cat_outputs.concat(other.datatypes); - if (other.node.post_job_actions){ - for (var pja_i in other.node.post_job_actions){ - var pja = other.node.post_job_actions[pja_i]; - if (pja.action_type == "ChangeDatatypeAction" && (pja.output_name == '' || pja.output_name == other.name) && pja.action_arguments){ - cat_outputs.push(pja.action_arguments['newtype']); - } - } - } - // FIXME: No idea what to do about case when datatype is 'input' - for ( var other_datatype_i in cat_outputs ) { - if ( cat_outputs[other_datatype_i] == "input" || issubtype( cat_outputs[other_datatype_i], this.datatypes[t] ) ) { - return true; - } - } - } - } - return false; +var OutputTerminal = Terminal.extend( { + initialize: function( attr ) { + Terminal.prototype.initialize.call( this, attr ); + this.datatypes = attr.datatypes; } -}); +} ); ////////////// @@ -309,7 +275,7 @@ var types = input.extensions; var multiple = input.multiple; - var terminal = this.el.terminal = new InputTerminal( this.el, types, multiple ); + var terminal = this.el.terminal = new InputTerminal( { element: this.el, datatypes: types, multiple: multiple } ); terminal.node = node; terminal.name = name; @@ -395,7 +361,7 @@ var element = this.el; var terminal_element = element; - var terminal = element.terminal = new OutputTerminal( element, type ); + var terminal = element.terminal = new OutputTerminal( {element: element, datatypes: type } ); terminal.node = node; terminal.name = name; node.output_terminals[name] = terminal; @@ -429,7 +395,7 @@ var h = $( '<div class="drag-terminal" style="position: absolute;"></div>' ) .appendTo( "#canvas-container" ).get(0); // Terminal and connection to display noodle while dragging - h.terminal = new OutputTerminal( h ); + h.terminal = new OutputTerminal( { element: h } ); var c = new Connector(); c.dragging = true; c.connect( this.el.terminal, h.terminal ); @@ -452,6 +418,40 @@ //////////// + +var InputTerminal = Terminal.extend( { + initialize: function( attr ) { + Terminal.prototype.initialize.call( this, attr ); + this.datatypes = attr.datatypes; + this.multiple = attr.multiple; + }, + can_accept: function ( other ) { + if ( this.connectors.length < 1 || this.multiple) { + for ( var t in this.datatypes ) { + var cat_outputs = new Array(); + cat_outputs = cat_outputs.concat(other.datatypes); + if (other.node.post_job_actions){ + for (var pja_i in other.node.post_job_actions){ + var pja = other.node.post_job_actions[pja_i]; + if (pja.action_type == "ChangeDatatypeAction" && (pja.output_name == '' || pja.output_name == other.name) && pja.action_arguments){ + cat_outputs.push(pja.action_arguments['newtype']); + } + } + } + // FIXME: No idea what to do about case when datatype is 'input' + for ( var other_datatype_i in cat_outputs ) { + if ( cat_outputs[other_datatype_i] == "input" || issubtype( cat_outputs[other_datatype_i], this.datatypes[t] ) ) { + return true; + } + } + } + } + return false; + } +}); + + + function Connector( handle1, handle2 ) { this.canvas = null; this.dragging = false; diff -r 61b3c6c4be0e1c8d02ef9073d294b61db461f4b3 -r 4957c7b9a70979fa10f85619ad608b071827df66 test/qunit/tests/workflow_editor_tests.js --- a/test/qunit/tests/workflow_editor_tests.js +++ b/test/qunit/tests/workflow_editor_tests.js @@ -58,7 +58,7 @@ setup: function() { this.node = { }; this.element = $( "<div>" ); - this.input_terminal = new InputTerminal( this.element, [ "txt" ] ); + this.input_terminal = new InputTerminal( { element: this.element, datatypes: [ "txt" ] } ); this.input_terminal.node = this.node; }, test_connector: function( attr ) { https://bitbucket.org/galaxy/galaxy-central/commits/f94426190a7b/ Changeset: f94426190a7b User: jmchilton Date: 2014-04-10 22:46:07 Summary: Workflow editor backbonification - reposition views to more appropriate part of file. Affected #: 1 file diff -r 4957c7b9a70979fa10f85619ad608b071827df66 -r f94426190a7b11837597efcf597aac288ae2dfe5 static/scripts/galaxy.workflow_editor.canvas.js --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -35,390 +35,6 @@ } ); -////////////// -// START VIEWS -////////////// - - - -var NodeView = Backbone.View.extend( { - initialize: function( options ){ - this.node = options.node; - this.output_width = Math.max(150, this.$el.width()); - this.tool_body = this.$el.find( ".toolFormBody" ); - this.tool_body.find( "div" ).remove(); - this.newInputsDiv().appendTo( this.tool_body ); - }, - - render : function() { - this.renderToolErrors(); - this.$el.css( "width", Math.min(250, Math.max(this.$el.width(), this.output_width ))); - }, - - renderToolErrors: function( ) { - if ( this.node.tool_errors ) { - this.$el.addClass( "tool-node-error" ); - } else { - this.$el.removeClass( "tool-node-error" ); - } - }, - - newInputsDiv: function() { - return $("<div class='inputs'></div>"); - }, - - updateMaxWidth: function( newWidth ) { - this.output_width = Math.max( this.output_width, newWidth ); - }, - - addRule: function() { - this.tool_body.append( $( "<div class='rule'></div>" ) ); - }, - - addDataInput: function( input ) { - var terminalView = new InputTerminalView( { - node: this.node, - input: input - } ); - var terminalElement = terminalView.el; - var inputView = new DataInputView( { - "terminalElement": terminalElement, - "input": input, - "nodeView": this, - } ); - var ib = inputView.$el; - var terminalElement = inputView.terminalElement; - this.$( ".inputs" ).append( ib.prepend( terminalElement ) ); - }, - - replaceDataInput: function( input, new_body ) { - var terminalView = new InputTerminalView( { - node: this.node, - input: input - } ); - var t = terminalView.el; - - // If already connected save old connection - this.$( "div[name='" + input.name + "']" ).each( function() { - $(this).find( ".input-terminal" ).each( function() { - var c = this.terminal.connectors[0]; - if ( c ) { - t.terminal.connectors[0] = c; - c.handle2 = t.terminal; - } - }); - $(this).remove(); - }); - var inputView = new DataInputView( { - "terminalElement": t, - "input": input, - "nodeView": this, - "skipResize": true, - } ); - var ib = inputView.$el; - - // Append to new body - new_body.append( ib.prepend( t ) ); - - }, - - addDataOutput: function( output ) { - var terminalView = new OutputTerminalView( { - node: this.node, - output: output - } ); - var outputView = new DataOutputView( { - "output": output, - "terminalElement": terminalView.el, - "nodeView": this, - } ); - this.tool_body.append( outputView.$el.append( outputView.terminalElement ) ); - } - -} ); - - - -var DataInputView = Backbone.View.extend( { - className: "form-row dataRow input-data-row", - - initialize: function( options ){ - this.input = options.input; - this.nodeView = options.nodeView; - this.terminalElement = options.terminalElement; - - this.$el.attr( "name", this.input.name ) - .html( this.input.label ); - - if( ! options.skipResize ) { - this.$el.css({ position:'absolute', - left: -1000, - top: -1000, - display:'none'}); - $('body').append(this.el); - this.nodeView.updateMaxWidth( this.$el.outerWidth() ); - this.$el.css({ position:'', - left:'', - top:'', - display:'' }); - this.$el.remove(); - } - }, - -} ); - - - -var OutputCalloutView = Backbone.View.extend( { - tagName: "div", - - initialize: function( options ) { - this.label = options.label; - this.node = options.node; - this.output = options.output; - - var view = this; - this.$el - .attr( "class", 'callout '+this.label ) - .css( { display: 'none' } ) - .append( - $("<div class='buttons'></div>").append( - $("<img/>").attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png').click( function() { - if ($.inArray(view.output.name, view.node.workflow_outputs) != -1){ - view.node.workflow_outputs.splice($.inArray(view.output.name, view.node.workflow_outputs), 1); - view.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png'); - }else{ - view.node.workflow_outputs.push(view.output.name); - view.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png'); - } - workflow.has_changes = true; - canvas_manager.draw_overview(); - }))) - .tooltip({delay:500, title: "Mark dataset as a workflow output. All unmarked datasets will be hidden." }); - - this.$el.css({ - top: '50%', - margin:'-8px 0px 0px 0px', - right: 8 - }); - this.$el.show(); - this.resetImage(); - }, - - resetImage: function() { - if ($.inArray( this.output.name, this.node.workflow_outputs) === -1){ - this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png'); - } else{ - this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png'); - } - }, - - hoverImage: function() { - this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-yellow.png'); - } - -} ); - - - - -var DataOutputView = Backbone.View.extend( { - className: "form-row dataRow", - - initialize: function( options ) { - this.output = options.output; - this.terminalElement = options.terminalElement; - this.nodeView = options.nodeView; - - var output = this.output; - var label = output.name; - var node = this.nodeView.node; - if ( output.extensions.indexOf( 'input' ) < 0 ) { - label = label + " (" + output.extensions.join(", ") + ")"; - } - this.$el.html( label ) - - if (node.type == 'tool'){ - var calloutView = new OutputCalloutView( { - "label": label, - "output": output, - "node": node, - }); - this.$el.append( calloutView.el ); - this.$el.hover( function() { calloutView.hoverImage() }, function() { calloutView.resetImage() } ); - } - this.$el.css({ position:'absolute', - left: -1000, - top: -1000, - display:'none'}); - $('body').append( this.el ); - this.nodeView.updateMaxWidth( this.$el.outerWidth() + 17 ); - this.$el.css({ position:'', - left:'', - top:'', - display:'' }) - .detach(); - } - -} ); - - - -var InputTerminalView = Backbone.View.extend( { - className: "terminal input-terminal", - - initialize: function( options ) { - var node = options.node; - var input = options.input; - - var name = input.name; - var types = input.extensions; - var multiple = input.multiple; - - var terminal = this.el.terminal = new InputTerminal( { element: this.el, datatypes: types, multiple: multiple } ); - terminal.node = node; - terminal.name = name; - - node.input_terminals[name] = terminal; - }, - - events: { - "dropinit": "onDropInit", - "dropstart": "onDropStart", - "dropend": "onDropEnd", - "drop": "onDrop", - "hover": "onHover", - }, - - onDropInit: function( e, d ) { - var terminal = this.el.terminal; - // Accept a dragable if it is an output terminal and has a - // compatible type - return $(d.drag).hasClass( "output-terminal" ) && terminal.can_accept( d.drag.terminal ); - }, - - onDropStart: function( e, d ) { - if (d.proxy.terminal) { - d.proxy.terminal.connectors[0].inner_color = "#BBFFBB"; - } - }, - - onDropEnd: function ( e, d ) { - if (d.proxy.terminal) { - d.proxy.terminal.connectors[0].inner_color = "#FFFFFF"; - } - }, - - onDrop: function( e, d ) { - var terminal = this.el.terminal; - new Connector( d.drag.terminal, terminal ).redraw(); - }, - - onHover: function() { - var element = this.el; - var terminal = element.terminal; - - // If connected, create a popup to allow disconnection - if ( terminal.connectors.length > 0 ) { - // Create callout - var t = $("<div class='callout'></div>") - .css( { display: 'none' } ) - .appendTo( "body" ) - .append( - $("<div class='button'></div>").append( - $("<div/>").addClass("fa-icon-button fa fa-times").click( function() { - $.each( terminal.connectors, function( _, x ) { - if (x) { - x.destroy(); - } - }); - t.remove(); - }))) - .bind( "mouseleave", function() { - $(this).remove(); - }); - // Position it and show - t.css({ - top: $(element).offset().top - 2, - left: $(element).offset().left - t.width(), - 'padding-right': $(element).width() - }).show(); - } - }, - -} ); - - - -var OutputTerminalView = Backbone.View.extend( { - className: "terminal output-terminal", - - initialize: function( options ) { - var node = options.node; - var output = options.output; - var name = output.name; - var type = output.extensions; - - var element = this.el; - var terminal_element = element; - var terminal = element.terminal = new OutputTerminal( {element: element, datatypes: type } ); - terminal.node = node; - terminal.name = name; - node.output_terminals[name] = terminal; - }, - - events: { - "drag": "onDrag", - "dragstart": "onDragStart", - "dragend": "onDragEnd", - }, - - onDrag: function ( e, d ) { - var onmove = function() { - var po = $(d.proxy).offsetParent().offset(), - x = d.offsetX - po.left, - y = d.offsetY - po.top; - $(d.proxy).css( { left: x, top: y } ); - d.proxy.terminal.redraw(); - // FIXME: global - canvas_manager.update_viewport_overlay(); - }; - onmove(); - $("#canvas-container").get(0).scroll_panel.test( e, onmove ); - }, - - onDragStart: function( e, d ) { - $( d.available ).addClass( "input-terminal-active" ); - // Save PJAs in the case of change datatype actions. - workflow.check_changes_in_active_form(); - // Drag proxy div - var h = $( '<div class="drag-terminal" style="position: absolute;"></div>' ) - .appendTo( "#canvas-container" ).get(0); - // Terminal and connection to display noodle while dragging - h.terminal = new OutputTerminal( { element: h } ); - var c = new Connector(); - c.dragging = true; - c.connect( this.el.terminal, h.terminal ); - return h; - }, - - onDragEnd: function ( e, d ) { - d.proxy.terminal.connectors[0].destroy(); - $(d.proxy).remove(); - $( d.available ).removeClass( "input-terminal-active" ); - $("#canvas-container").get(0).scroll_panel.stop(); - } - -} ); - - - -//////////// -// END VIEWS -//////////// - - - var InputTerminal = Terminal.extend( { initialize: function( attr ) { Terminal.prototype.initialize.call( this, attr ); @@ -1114,6 +730,389 @@ type_to_type = data.class_to_classes; } + +////////////// +// START VIEWS +////////////// + + +var NodeView = Backbone.View.extend( { + initialize: function( options ){ + this.node = options.node; + this.output_width = Math.max(150, this.$el.width()); + this.tool_body = this.$el.find( ".toolFormBody" ); + this.tool_body.find( "div" ).remove(); + this.newInputsDiv().appendTo( this.tool_body ); + }, + + render : function() { + this.renderToolErrors(); + this.$el.css( "width", Math.min(250, Math.max(this.$el.width(), this.output_width ))); + }, + + renderToolErrors: function( ) { + if ( this.node.tool_errors ) { + this.$el.addClass( "tool-node-error" ); + } else { + this.$el.removeClass( "tool-node-error" ); + } + }, + + newInputsDiv: function() { + return $("<div class='inputs'></div>"); + }, + + updateMaxWidth: function( newWidth ) { + this.output_width = Math.max( this.output_width, newWidth ); + }, + + addRule: function() { + this.tool_body.append( $( "<div class='rule'></div>" ) ); + }, + + addDataInput: function( input ) { + var terminalView = new InputTerminalView( { + node: this.node, + input: input + } ); + var terminalElement = terminalView.el; + var inputView = new DataInputView( { + "terminalElement": terminalElement, + "input": input, + "nodeView": this, + } ); + var ib = inputView.$el; + var terminalElement = inputView.terminalElement; + this.$( ".inputs" ).append( ib.prepend( terminalElement ) ); + }, + + replaceDataInput: function( input, new_body ) { + var terminalView = new InputTerminalView( { + node: this.node, + input: input + } ); + var t = terminalView.el; + + // If already connected save old connection + this.$( "div[name='" + input.name + "']" ).each( function() { + $(this).find( ".input-terminal" ).each( function() { + var c = this.terminal.connectors[0]; + if ( c ) { + t.terminal.connectors[0] = c; + c.handle2 = t.terminal; + } + }); + $(this).remove(); + }); + var inputView = new DataInputView( { + "terminalElement": t, + "input": input, + "nodeView": this, + "skipResize": true, + } ); + var ib = inputView.$el; + + // Append to new body + new_body.append( ib.prepend( t ) ); + + }, + + addDataOutput: function( output ) { + var terminalView = new OutputTerminalView( { + node: this.node, + output: output + } ); + var outputView = new DataOutputView( { + "output": output, + "terminalElement": terminalView.el, + "nodeView": this, + } ); + this.tool_body.append( outputView.$el.append( outputView.terminalElement ) ); + } + +} ); + + + +var DataInputView = Backbone.View.extend( { + className: "form-row dataRow input-data-row", + + initialize: function( options ){ + this.input = options.input; + this.nodeView = options.nodeView; + this.terminalElement = options.terminalElement; + + this.$el.attr( "name", this.input.name ) + .html( this.input.label ); + + if( ! options.skipResize ) { + this.$el.css({ position:'absolute', + left: -1000, + top: -1000, + display:'none'}); + $('body').append(this.el); + this.nodeView.updateMaxWidth( this.$el.outerWidth() ); + this.$el.css({ position:'', + left:'', + top:'', + display:'' }); + this.$el.remove(); + } + }, + +} ); + + + +var OutputCalloutView = Backbone.View.extend( { + tagName: "div", + + initialize: function( options ) { + this.label = options.label; + this.node = options.node; + this.output = options.output; + + var view = this; + this.$el + .attr( "class", 'callout '+this.label ) + .css( { display: 'none' } ) + .append( + $("<div class='buttons'></div>").append( + $("<img/>").attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png').click( function() { + if ($.inArray(view.output.name, view.node.workflow_outputs) != -1){ + view.node.workflow_outputs.splice($.inArray(view.output.name, view.node.workflow_outputs), 1); + view.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png'); + }else{ + view.node.workflow_outputs.push(view.output.name); + view.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png'); + } + workflow.has_changes = true; + canvas_manager.draw_overview(); + }))) + .tooltip({delay:500, title: "Mark dataset as a workflow output. All unmarked datasets will be hidden." }); + + this.$el.css({ + top: '50%', + margin:'-8px 0px 0px 0px', + right: 8 + }); + this.$el.show(); + this.resetImage(); + }, + + resetImage: function() { + if ($.inArray( this.output.name, this.node.workflow_outputs) === -1){ + this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png'); + } else{ + this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png'); + } + }, + + hoverImage: function() { + this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-yellow.png'); + } + +} ); + + + + +var DataOutputView = Backbone.View.extend( { + className: "form-row dataRow", + + initialize: function( options ) { + this.output = options.output; + this.terminalElement = options.terminalElement; + this.nodeView = options.nodeView; + + var output = this.output; + var label = output.name; + var node = this.nodeView.node; + if ( output.extensions.indexOf( 'input' ) < 0 ) { + label = label + " (" + output.extensions.join(", ") + ")"; + } + this.$el.html( label ) + + if (node.type == 'tool'){ + var calloutView = new OutputCalloutView( { + "label": label, + "output": output, + "node": node, + }); + this.$el.append( calloutView.el ); + this.$el.hover( function() { calloutView.hoverImage() }, function() { calloutView.resetImage() } ); + } + this.$el.css({ position:'absolute', + left: -1000, + top: -1000, + display:'none'}); + $('body').append( this.el ); + this.nodeView.updateMaxWidth( this.$el.outerWidth() + 17 ); + this.$el.css({ position:'', + left:'', + top:'', + display:'' }) + .detach(); + } + +} ); + + + +var InputTerminalView = Backbone.View.extend( { + className: "terminal input-terminal", + + initialize: function( options ) { + var node = options.node; + var input = options.input; + + var name = input.name; + var types = input.extensions; + var multiple = input.multiple; + + var terminal = this.el.terminal = new InputTerminal( { element: this.el, datatypes: types, multiple: multiple } ); + terminal.node = node; + terminal.name = name; + + node.input_terminals[name] = terminal; + }, + + events: { + "dropinit": "onDropInit", + "dropstart": "onDropStart", + "dropend": "onDropEnd", + "drop": "onDrop", + "hover": "onHover", + }, + + onDropInit: function( e, d ) { + var terminal = this.el.terminal; + // Accept a dragable if it is an output terminal and has a + // compatible type + return $(d.drag).hasClass( "output-terminal" ) && terminal.can_accept( d.drag.terminal ); + }, + + onDropStart: function( e, d ) { + if (d.proxy.terminal) { + d.proxy.terminal.connectors[0].inner_color = "#BBFFBB"; + } + }, + + onDropEnd: function ( e, d ) { + if (d.proxy.terminal) { + d.proxy.terminal.connectors[0].inner_color = "#FFFFFF"; + } + }, + + onDrop: function( e, d ) { + var terminal = this.el.terminal; + new Connector( d.drag.terminal, terminal ).redraw(); + }, + + onHover: function() { + var element = this.el; + var terminal = element.terminal; + + // If connected, create a popup to allow disconnection + if ( terminal.connectors.length > 0 ) { + // Create callout + var t = $("<div class='callout'></div>") + .css( { display: 'none' } ) + .appendTo( "body" ) + .append( + $("<div class='button'></div>").append( + $("<div/>").addClass("fa-icon-button fa fa-times").click( function() { + $.each( terminal.connectors, function( _, x ) { + if (x) { + x.destroy(); + } + }); + t.remove(); + }))) + .bind( "mouseleave", function() { + $(this).remove(); + }); + // Position it and show + t.css({ + top: $(element).offset().top - 2, + left: $(element).offset().left - t.width(), + 'padding-right': $(element).width() + }).show(); + } + }, + +} ); + + + +var OutputTerminalView = Backbone.View.extend( { + className: "terminal output-terminal", + + initialize: function( options ) { + var node = options.node; + var output = options.output; + var name = output.name; + var type = output.extensions; + + var element = this.el; + var terminal_element = element; + var terminal = element.terminal = new OutputTerminal( {element: element, datatypes: type } ); + terminal.node = node; + terminal.name = name; + node.output_terminals[name] = terminal; + }, + + events: { + "drag": "onDrag", + "dragstart": "onDragStart", + "dragend": "onDragEnd", + }, + + onDrag: function ( e, d ) { + var onmove = function() { + var po = $(d.proxy).offsetParent().offset(), + x = d.offsetX - po.left, + y = d.offsetY - po.top; + $(d.proxy).css( { left: x, top: y } ); + d.proxy.terminal.redraw(); + // FIXME: global + canvas_manager.update_viewport_overlay(); + }; + onmove(); + $("#canvas-container").get(0).scroll_panel.test( e, onmove ); + }, + + onDragStart: function( e, d ) { + $( d.available ).addClass( "input-terminal-active" ); + // Save PJAs in the case of change datatype actions. + workflow.check_changes_in_active_form(); + // Drag proxy div + var h = $( '<div class="drag-terminal" style="position: absolute;"></div>' ) + .appendTo( "#canvas-container" ).get(0); + // Terminal and connection to display noodle while dragging + h.terminal = new OutputTerminal( { element: h } ); + var c = new Connector(); + c.dragging = true; + c.connect( this.el.terminal, h.terminal ); + return h; + }, + + onDragEnd: function ( e, d ) { + d.proxy.terminal.connectors[0].destroy(); + $(d.proxy).remove(); + $( d.available ).removeClass( "input-terminal-active" ); + $("#canvas-container").get(0).scroll_panel.stop(); + } + +} ); + + + +//////////// +// END VIEWS +//////////// + + // FIXME: merge scroll panel into CanvasManager, clean up hardcoded stuff. function ScrollPanel( panel ) { https://bitbucket.org/galaxy/galaxy-central/commits/991f6ba12df4/ Changeset: 991f6ba12df4 User: jmchilton Date: 2014-04-19 18:57:53 Summary: Merge pull request #363. Was previously declined so that pull request #370 could be resolved first - but upon actually inspecting #370 I don't think these will conflict in anyway. Affected #: 2 files diff -r 439510f01e3b0850fd844cded14977076ec86501 -r 991f6ba12df4dddcded9a813bd5e55e70ad31841 static/scripts/galaxy.workflow_editor.canvas.js --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -1,18 +1,18 @@ -function Terminal( element ) { - this.element = element; - this.connectors = []; -} -$.extend( Terminal.prototype, { +var Terminal = Backbone.Model.extend( { + initialize: function( attr ) { + this.element = attr.element; + this.connectors = []; + }, connect: function ( connector ) { this.connectors.push( connector ); if ( this.node ) { - this.node.changed(); + this.node.markChanged(); } }, disconnect: function ( connector ) { this.connectors.splice( $.inArray( connector, this.connectors ), 1 ); if ( this.node ) { - this.node.changed(); + this.node.markChanged(); } }, redraw: function () { @@ -25,24 +25,22 @@ c.destroy(); }); } -}); +} ); -function OutputTerminal( element, datatypes ) { - Terminal.call( this, element ); - this.datatypes = datatypes; -} +var OutputTerminal = Terminal.extend( { + initialize: function( attr ) { + Terminal.prototype.initialize.call( this, attr ); + this.datatypes = attr.datatypes; + } +} ); -OutputTerminal.prototype = new Terminal(); -function InputTerminal( element, datatypes, multiple ) { - Terminal.call( this, element ); - this.datatypes = datatypes; - this.multiple = multiple -} - -InputTerminal.prototype = new Terminal(); - -$.extend( InputTerminal.prototype, { +var InputTerminal = Terminal.extend( { + initialize: function( attr ) { + Terminal.prototype.initialize.call( this, attr ); + this.datatypes = attr.datatypes; + this.multiple = attr.multiple; + }, can_accept: function ( other ) { if ( this.connectors.length < 1 || this.multiple) { for ( var t in this.datatypes ) { @@ -68,6 +66,8 @@ } }); + + function Connector( handle1, handle2 ) { this.canvas = null; this.dragging = false; @@ -164,106 +164,13 @@ } } ); -function Node( element ) { - this.element = element; - this.input_terminals = {}; - this.output_terminals = {}; - this.tool_errors = {}; -} -$.extend( Node.prototype, { - new_input_terminal : function( input ) { - var t = $("<div class='terminal input-terminal'></div>")[ 0 ]; - this.enable_input_terminal( t, input.name, input.extensions, input.multiple ); - return t; - }, - enable_input_terminal : function( element, name, types, multiple ) { - var node = this; +var Node = Backbone.Model.extend({ - var terminal = element.terminal = new InputTerminal( element, types, multiple ); - terminal.node = node; - terminal.name = name; - $(element).bind( "dropinit", function( e, d ) { - // Accept a dragable if it is an output terminal and has a - // compatible type - return $(d.drag).hasClass( "output-terminal" ) && terminal.can_accept( d.drag.terminal ); - }).bind( "dropstart", function( e, d ) { - if (d.proxy.terminal) { - d.proxy.terminal.connectors[0].inner_color = "#BBFFBB"; - } - }).bind( "dropend", function ( e, d ) { - if (d.proxy.terminal) { - d.proxy.terminal.connectors[0].inner_color = "#FFFFFF"; - } - }).bind( "drop", function( e, d ) { - ( new Connector( d.drag.terminal, terminal ) ).redraw(); - }).bind( "hover", function() { - // If connected, create a popup to allow disconnection - if ( terminal.connectors.length > 0 ) { - // Create callout - var t = $("<div class='callout'></div>") - .css( { display: 'none' } ) - .appendTo( "body" ) - .append( - $("<div class='button'></div>").append( - $("<div/>").addClass("fa-icon-button fa fa-times").click( function() { - $.each( terminal.connectors, function( _, x ) { - if (x) { - x.destroy(); - } - }); - t.remove(); - }))) - .bind( "mouseleave", function() { - $(this).remove(); - }); - // Position it and show - t.css({ - top: $(element).offset().top - 2, - left: $(element).offset().left - t.width(), - 'padding-right': $(element).width() - }).show(); - } - }); - node.input_terminals[name] = terminal; - }, - enable_output_terminal : function( element, name, type ) { - var node = this; - var terminal_element = element; - var terminal = element.terminal = new OutputTerminal( element, type ); - terminal.node = node; - terminal.name = name; - $(element).bind( "dragstart", function( e, d ) { - $( d.available ).addClass( "input-terminal-active" ); - // Save PJAs in the case of change datatype actions. - workflow.check_changes_in_active_form(); - // Drag proxy div - var h = $( '<div class="drag-terminal" style="position: absolute;"></div>' ) - .appendTo( "#canvas-container" ).get(0); - // Terminal and connection to display noodle while dragging - h.terminal = new OutputTerminal( h ); - var c = new Connector(); - c.dragging = true; - c.connect( element.terminal, h.terminal ); - return h; - }).bind( "drag", function ( e, d ) { - var onmove = function() { - var po = $(d.proxy).offsetParent().offset(), - x = d.offsetX - po.left, - y = d.offsetY - po.top; - $(d.proxy).css( { left: x, top: y } ); - d.proxy.terminal.redraw(); - // FIXME: global - canvas_manager.update_viewport_overlay(); - }; - onmove(); - $("#canvas-container").get(0).scroll_panel.test( e, onmove ); - }).bind( "dragend", function ( e, d ) { - d.proxy.terminal.connectors[0].destroy(); - $(d.proxy).remove(); - $( d.available ).removeClass( "input-terminal-active" ); - $("#canvas-container").get(0).scroll_panel.stop(); - }); - node.output_terminals[name] = terminal; + initialize: function( attr ) { + this.element = attr.element; + this.input_terminals = {}; + this.output_terminals = {}; + this.tool_errors = {}; }, redraw : function () { $.each( this.input_terminals, function( _, t ) { @@ -295,7 +202,6 @@ $(element).removeClass( "toolForm-active" ); }, init_field_data : function ( data ) { - var f = this.element; if ( data.type ) { this.type = data.type; } @@ -308,132 +214,40 @@ this.post_job_actions = data.post_job_actions ? data.post_job_actions : {}; this.workflow_outputs = data.workflow_outputs ? data.workflow_outputs : []; - if ( this.tool_errors ) { - f.addClass( "tool-node-error" ); - } else { - f.removeClass( "tool-node-error" ); - } var node = this; - var output_width = Math.max(150, f.width()); - var b = f.find( ".toolFormBody" ); - b.find( "div" ).remove(); - var ibox = $("<div class='inputs'></div>").appendTo( b ); + var nodeView = new NodeView({ + el: this.element[ 0 ], + node: node, + }); + node.nodeView = nodeView; + $.each( data.data_inputs, function( i, input ) { - var t = node.new_input_terminal( input ); - var ib = $("<div class='form-row dataRow input-data-row' name='" + input.name + "'>" + input.label + "</div>" ); - ib.css({ position:'absolute', - left: -1000, - top: -1000, - display:'none'}); - $('body').append(ib); - output_width = Math.max(output_width, ib.outerWidth()); - ib.css({ position:'', - left:'', - top:'', - display:'' }); - ib.remove(); - ibox.append( ib.prepend( t ) ); + nodeView.addDataInput( input ); }); if ( ( data.data_inputs.length > 0 ) && ( data.data_outputs.length > 0 ) ) { - b.append( $( "<div class='rule'></div>" ) ); + nodeView.addRule(); } $.each( data.data_outputs, function( i, output ) { - var t = $( "<div class='terminal output-terminal'></div>" ); - node.enable_output_terminal( t[ 0 ], output.name, output.extensions ); - var label = output.name; - if ( output.extensions.indexOf( 'input' ) < 0 ) { - label = label + " (" + output.extensions.join(", ") + ")"; - } - var r = $("<div class='form-row dataRow'>" + label + "</div>" ); - if (node.type == 'tool'){ - var callout = $("<div class='callout "+label+"'></div>") - .css( { display: 'none' } ) - .append( - $("<div class='buttons'></div>").append( - $("<img/>").attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png').click( function() { - if ($.inArray(output.name, node.workflow_outputs) != -1){ - node.workflow_outputs.splice($.inArray(output.name, node.workflow_outputs), 1); - callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png'); - }else{ - node.workflow_outputs.push(output.name); - callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png'); - } - workflow.has_changes = true; - canvas_manager.draw_overview(); - }))) - .tooltip({delay:500, title: "Mark dataset as a workflow output. All unmarked datasets will be hidden." }); - callout.css({ - top: '50%', - margin:'-8px 0px 0px 0px', - right: 8 - }); - callout.show(); - r.append(callout); - if ($.inArray(output.name, node.workflow_outputs) === -1){ - callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png'); - }else{ - callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png'); - } - r.hover( - function(){ - callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-yellow.png'); - }, - function(){ - if ($.inArray(output.name, node.workflow_outputs) === -1){ - callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png'); - }else{ - callout.find('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png'); - } - }); - } - r.css({ position:'absolute', - left: -1000, - top: -1000, - display:'none'}); - $('body').append(r); - output_width = Math.max(output_width, r.outerWidth() + 17); - r.css({ position:'', - left:'', - top:'', - display:'' }); - r.detach(); - b.append( r.append( t ) ); - }); - f.css( "width", Math.min(250, Math.max(f.width(), output_width ))); + nodeView.addDataOutput( output ); + } ); + nodeView.render(); workflow.node_changed( this ); }, update_field_data : function( data ) { - var el = $(this.element), - node = this; + var node = this; + nodeView = node.nodeView; this.tool_state = data.tool_state; this.form_html = data.form_html; this.tool_errors = data.tool_errors; this.annotation = data['annotation']; var pja_in = $.parseJSON(data.post_job_actions); this.post_job_actions = pja_in ? pja_in : {}; - if ( this.tool_errors ) { - el.addClass( "tool-node-error" ); - } else { - el.removeClass( "tool-node-error" ); - } + node.nodeView.renderToolErrors(); // Update input rows - var old_body = el.find( "div.inputs" ); - var new_body = $("<div class='inputs'></div>"); + var old_body = nodeView.$( "div.inputs" ); + var new_body = nodeView.newInputsDiv(); $.each( data.data_inputs, function( i, input ) { - var t = node.new_input_terminal( input ); - // If already connected save old connection - old_body.find( "div[name='" + input.name + "']" ).each( function() { - $(this).find( ".input-terminal" ).each( function() { - var c = this.terminal.connectors[0]; - if ( c ) { - t.terminal.connectors[0] = c; - c.handle2 = t.terminal; - } - }); - $(this).remove(); - }); - // Append to new body - new_body.append( $("<div class='form-row dataRow input-data-row' name='" + input.name + "'>" + input.label + "</div>" ).prepend( t ) ); + node.nodeView.replaceDataInput( input, new_body ); }); old_body.replaceWith( new_body ); // Cleanup any leftover terminals @@ -441,7 +255,7 @@ this.terminal.destroy(); }); // If active, reactivate with new form_html - this.changed(); + this.markChanged(); this.redraw(); }, error : function ( text ) { @@ -452,7 +266,7 @@ b.html( tmp ); workflow.node_changed( this ); }, - changed: function() { + markChanged: function() { workflow.node_changed( this ); } } ); @@ -837,7 +651,7 @@ function prebuild_node( type, title_text, tool_id ) { var f = $("<div class='toolForm toolFormInCanvas'></div>"); - var node = new Node( f ); + var node = new Node( { element: f } ); node.type = type; if ( type == 'tool' ) { node.tool_id = tool_id; @@ -916,6 +730,389 @@ type_to_type = data.class_to_classes; } + +////////////// +// START VIEWS +////////////// + + +var NodeView = Backbone.View.extend( { + initialize: function( options ){ + this.node = options.node; + this.output_width = Math.max(150, this.$el.width()); + this.tool_body = this.$el.find( ".toolFormBody" ); + this.tool_body.find( "div" ).remove(); + this.newInputsDiv().appendTo( this.tool_body ); + }, + + render : function() { + this.renderToolErrors(); + this.$el.css( "width", Math.min(250, Math.max(this.$el.width(), this.output_width ))); + }, + + renderToolErrors: function( ) { + if ( this.node.tool_errors ) { + this.$el.addClass( "tool-node-error" ); + } else { + this.$el.removeClass( "tool-node-error" ); + } + }, + + newInputsDiv: function() { + return $("<div class='inputs'></div>"); + }, + + updateMaxWidth: function( newWidth ) { + this.output_width = Math.max( this.output_width, newWidth ); + }, + + addRule: function() { + this.tool_body.append( $( "<div class='rule'></div>" ) ); + }, + + addDataInput: function( input ) { + var terminalView = new InputTerminalView( { + node: this.node, + input: input + } ); + var terminalElement = terminalView.el; + var inputView = new DataInputView( { + "terminalElement": terminalElement, + "input": input, + "nodeView": this, + } ); + var ib = inputView.$el; + var terminalElement = inputView.terminalElement; + this.$( ".inputs" ).append( ib.prepend( terminalElement ) ); + }, + + replaceDataInput: function( input, new_body ) { + var terminalView = new InputTerminalView( { + node: this.node, + input: input + } ); + var t = terminalView.el; + + // If already connected save old connection + this.$( "div[name='" + input.name + "']" ).each( function() { + $(this).find( ".input-terminal" ).each( function() { + var c = this.terminal.connectors[0]; + if ( c ) { + t.terminal.connectors[0] = c; + c.handle2 = t.terminal; + } + }); + $(this).remove(); + }); + var inputView = new DataInputView( { + "terminalElement": t, + "input": input, + "nodeView": this, + "skipResize": true, + } ); + var ib = inputView.$el; + + // Append to new body + new_body.append( ib.prepend( t ) ); + + }, + + addDataOutput: function( output ) { + var terminalView = new OutputTerminalView( { + node: this.node, + output: output + } ); + var outputView = new DataOutputView( { + "output": output, + "terminalElement": terminalView.el, + "nodeView": this, + } ); + this.tool_body.append( outputView.$el.append( outputView.terminalElement ) ); + } + +} ); + + + +var DataInputView = Backbone.View.extend( { + className: "form-row dataRow input-data-row", + + initialize: function( options ){ + this.input = options.input; + this.nodeView = options.nodeView; + this.terminalElement = options.terminalElement; + + this.$el.attr( "name", this.input.name ) + .html( this.input.label ); + + if( ! options.skipResize ) { + this.$el.css({ position:'absolute', + left: -1000, + top: -1000, + display:'none'}); + $('body').append(this.el); + this.nodeView.updateMaxWidth( this.$el.outerWidth() ); + this.$el.css({ position:'', + left:'', + top:'', + display:'' }); + this.$el.remove(); + } + }, + +} ); + + + +var OutputCalloutView = Backbone.View.extend( { + tagName: "div", + + initialize: function( options ) { + this.label = options.label; + this.node = options.node; + this.output = options.output; + + var view = this; + this.$el + .attr( "class", 'callout '+this.label ) + .css( { display: 'none' } ) + .append( + $("<div class='buttons'></div>").append( + $("<img/>").attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png').click( function() { + if ($.inArray(view.output.name, view.node.workflow_outputs) != -1){ + view.node.workflow_outputs.splice($.inArray(view.output.name, view.node.workflow_outputs), 1); + view.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png'); + }else{ + view.node.workflow_outputs.push(view.output.name); + view.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png'); + } + workflow.has_changes = true; + canvas_manager.draw_overview(); + }))) + .tooltip({delay:500, title: "Mark dataset as a workflow output. All unmarked datasets will be hidden." }); + + this.$el.css({ + top: '50%', + margin:'-8px 0px 0px 0px', + right: 8 + }); + this.$el.show(); + this.resetImage(); + }, + + resetImage: function() { + if ($.inArray( this.output.name, this.node.workflow_outputs) === -1){ + this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-outline.png'); + } else{ + this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small.png'); + } + }, + + hoverImage: function() { + this.$('img').attr('src', galaxy_config.root + 'static/images/fugue/asterisk-small-yellow.png'); + } + +} ); + + + + +var DataOutputView = Backbone.View.extend( { + className: "form-row dataRow", + + initialize: function( options ) { + this.output = options.output; + this.terminalElement = options.terminalElement; + this.nodeView = options.nodeView; + + var output = this.output; + var label = output.name; + var node = this.nodeView.node; + if ( output.extensions.indexOf( 'input' ) < 0 ) { + label = label + " (" + output.extensions.join(", ") + ")"; + } + this.$el.html( label ) + + if (node.type == 'tool'){ + var calloutView = new OutputCalloutView( { + "label": label, + "output": output, + "node": node, + }); + this.$el.append( calloutView.el ); + this.$el.hover( function() { calloutView.hoverImage() }, function() { calloutView.resetImage() } ); + } + this.$el.css({ position:'absolute', + left: -1000, + top: -1000, + display:'none'}); + $('body').append( this.el ); + this.nodeView.updateMaxWidth( this.$el.outerWidth() + 17 ); + this.$el.css({ position:'', + left:'', + top:'', + display:'' }) + .detach(); + } + +} ); + + + +var InputTerminalView = Backbone.View.extend( { + className: "terminal input-terminal", + + initialize: function( options ) { + var node = options.node; + var input = options.input; + + var name = input.name; + var types = input.extensions; + var multiple = input.multiple; + + var terminal = this.el.terminal = new InputTerminal( { element: this.el, datatypes: types, multiple: multiple } ); + terminal.node = node; + terminal.name = name; + + node.input_terminals[name] = terminal; + }, + + events: { + "dropinit": "onDropInit", + "dropstart": "onDropStart", + "dropend": "onDropEnd", + "drop": "onDrop", + "hover": "onHover", + }, + + onDropInit: function( e, d ) { + var terminal = this.el.terminal; + // Accept a dragable if it is an output terminal and has a + // compatible type + return $(d.drag).hasClass( "output-terminal" ) && terminal.can_accept( d.drag.terminal ); + }, + + onDropStart: function( e, d ) { + if (d.proxy.terminal) { + d.proxy.terminal.connectors[0].inner_color = "#BBFFBB"; + } + }, + + onDropEnd: function ( e, d ) { + if (d.proxy.terminal) { + d.proxy.terminal.connectors[0].inner_color = "#FFFFFF"; + } + }, + + onDrop: function( e, d ) { + var terminal = this.el.terminal; + new Connector( d.drag.terminal, terminal ).redraw(); + }, + + onHover: function() { + var element = this.el; + var terminal = element.terminal; + + // If connected, create a popup to allow disconnection + if ( terminal.connectors.length > 0 ) { + // Create callout + var t = $("<div class='callout'></div>") + .css( { display: 'none' } ) + .appendTo( "body" ) + .append( + $("<div class='button'></div>").append( + $("<div/>").addClass("fa-icon-button fa fa-times").click( function() { + $.each( terminal.connectors, function( _, x ) { + if (x) { + x.destroy(); + } + }); + t.remove(); + }))) + .bind( "mouseleave", function() { + $(this).remove(); + }); + // Position it and show + t.css({ + top: $(element).offset().top - 2, + left: $(element).offset().left - t.width(), + 'padding-right': $(element).width() + }).show(); + } + }, + +} ); + + + +var OutputTerminalView = Backbone.View.extend( { + className: "terminal output-terminal", + + initialize: function( options ) { + var node = options.node; + var output = options.output; + var name = output.name; + var type = output.extensions; + + var element = this.el; + var terminal_element = element; + var terminal = element.terminal = new OutputTerminal( {element: element, datatypes: type } ); + terminal.node = node; + terminal.name = name; + node.output_terminals[name] = terminal; + }, + + events: { + "drag": "onDrag", + "dragstart": "onDragStart", + "dragend": "onDragEnd", + }, + + onDrag: function ( e, d ) { + var onmove = function() { + var po = $(d.proxy).offsetParent().offset(), + x = d.offsetX - po.left, + y = d.offsetY - po.top; + $(d.proxy).css( { left: x, top: y } ); + d.proxy.terminal.redraw(); + // FIXME: global + canvas_manager.update_viewport_overlay(); + }; + onmove(); + $("#canvas-container").get(0).scroll_panel.test( e, onmove ); + }, + + onDragStart: function( e, d ) { + $( d.available ).addClass( "input-terminal-active" ); + // Save PJAs in the case of change datatype actions. + workflow.check_changes_in_active_form(); + // Drag proxy div + var h = $( '<div class="drag-terminal" style="position: absolute;"></div>' ) + .appendTo( "#canvas-container" ).get(0); + // Terminal and connection to display noodle while dragging + h.terminal = new OutputTerminal( { element: h } ); + var c = new Connector(); + c.dragging = true; + c.connect( this.el.terminal, h.terminal ); + return h; + }, + + onDragEnd: function ( e, d ) { + d.proxy.terminal.connectors[0].destroy(); + $(d.proxy).remove(); + $( d.available ).removeClass( "input-terminal-active" ); + $("#canvas-container").get(0).scroll_panel.stop(); + } + +} ); + + + +//////////// +// END VIEWS +//////////// + + // FIXME: merge scroll panel into CanvasManager, clean up hardcoded stuff. function ScrollPanel( panel ) { diff -r 439510f01e3b0850fd844cded14977076ec86501 -r 991f6ba12df4dddcded9a813bd5e55e70ad31841 test/qunit/tests/workflow_editor_tests.js --- a/test/qunit/tests/workflow_editor_tests.js +++ b/test/qunit/tests/workflow_editor_tests.js @@ -58,7 +58,7 @@ setup: function() { this.node = { }; this.element = $( "<div>" ); - this.input_terminal = new InputTerminal( this.element, [ "txt" ] ); + this.input_terminal = new InputTerminal( { element: this.element, datatypes: [ "txt" ] } ); this.input_terminal.node = this.node; }, test_connector: function( attr ) { @@ -81,25 +81,25 @@ } ); test( "test connect", function() { - this.node.changed = sinon.spy(); + this.node.markChanged = sinon.spy(); var connector = {}; this.input_terminal.connect( connector ); - // Assert node changed called - ok( this.node.changed.called ); + // Assert node markChanged called + ok( this.node.markChanged.called ); // Assert connectors updated ok( this.input_terminal.connectors[ 0 ] === connector ); } ); test( "test disconnect", function() { - this.node.changed = sinon.spy(); + this.node.markChanged = sinon.spy(); var connector = this.test_connector( {} ); this.input_terminal.disconnect( connector ); - // Assert node changed called - ok( this.node.changed.called ); + // Assert node markChanged called + ok( this.node.markChanged.called ); // Assert connectors updated equal( this.input_terminal.connectors.length, 0 ); } ); @@ -204,7 +204,7 @@ this.input_terminal = { destroy: sinon.spy(), redraw: sinon.spy() }; this.output_terminal = { destroy: sinon.spy(), redraw: sinon.spy() }; this.element = $("<div><div class='toolFormBody'></div></div>"); - this.node = new Node( this.element ); + this.node = new Node( { element: this.element } ); this.node.input_terminals.i1 = this.input_terminal; this.node.output_terminals.o1 = this.output_terminal; }, @@ -376,4 +376,92 @@ } ); } ); + /* global NodeView */ + module( "Node view ", { + setup: function() { + this.set_for_node( {} ); + }, + set_for_node: function( node ) { + var element = $("<div>"); + this.view = new NodeView( { node: node, el: element[ 0 ] } ); + }, + } ); + + test( "tool error styling", function() { + this.set_for_node( { tool_errors: false } ); + this.view.render(); + ok( ! this.view.$el.hasClass( "tool-node-error" ) ); + this.set_for_node( { tool_errors: true } ); + this.view.render(); + ok( this.view.$el.hasClass( "tool-node-error" ) ); + } ); + + test( "rendering correct width", function() { + // Default width is 150 + this.view.render(); + equal( this.view.$el.width(), 150 ); + + // If any data rows are greater, it will update + this.view.updateMaxWidth( 200 ); + this.view.render(); + equal( this.view.$el.width(), 200 ); + + // However 250 is the maximum width of node + this.view.updateMaxWidth( 300 ); + this.view.render(); + equal( this.view.$el.width(), 250 ); + + } ); + + /* global InputTerminalView */ + module( "Input terminal view", { + setup: function() { + this.node = { input_terminals: [] }; + this.input = { name: "i1", extensions: "txt", multiple: false }; + this.view = new InputTerminalView( { + node: this.node, + input: this.input, + }); + } + } ); + + test( "terminal added to node", function() { + ok( this.node.input_terminals.i1 ); + equal( this.node.input_terminals.i1.datatypes, [ "txt" ] ); + equal( this.node.input_terminals.i1.multiple, false ); + } ); + + test( "terminal element", function() { + var el = this.view.el; + equal( el.tagName, "DIV" ); + equal( el.className, "terminal input-terminal"); + } ); + + // TODO: Test binding... not sure how to do that exactly.. + + /* global OutputTerminalView */ + module( "Output terminal view", { + setup: function() { + this.node = { output_terminals: [] }; + this.output = { name: "o1", extensions: "txt" }; + this.view = new OutputTerminalView( { + node: this.node, + output: this.output, + }); + } + } ); + + test( "terminal added to node", function() { + ok( this.node.output_terminals.o1 ); + equal( this.node.output_terminals.o1.datatypes, [ "txt" ] ); + } ); + + test( "terminal element", function() { + var el = this.view.el; + equal( el.tagName, "DIV" ); + equal( el.className, "terminal output-terminal"); + } ); + + // TODO: Test bindings + }); \ No newline at end of file Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.
participants (1)
-
commits-noreply@bitbucket.org