From e0a4b42dae875eef198add63c3d826304a4dc52f Mon Sep 17 00:00:00 2001 From: James Taylor Date: Sat, 20 Oct 2007 03:11:20 +0000 Subject: [PATCH] Workflow editor round 3, steps now track their state properly, including repeat and conditional parameters. Dynamic selects (including columns) "work" but require values entered as text. These will be validated at runtime. --- lib/galaxy/tools/parameters.py | 15 + lib/galaxy/web/controllers/workflow_editor.py | 25 +- lib/galaxy/web/framework/__init__.py | 4 + .../scripts/galaxy.workflow_editor.canvas.js | 69 +- static/scripts/jquery.form.js | 819 ++++++++++++++++++ static/scripts/jquery.hoverIntent.js | 112 +++ templates/workflow_editor/canvas.mako | 13 +- templates/workflow_editor/index.mako | 47 +- 8 files changed, 1030 insertions(+), 74 deletions(-) create mode 100644 static/scripts/jquery.form.js create mode 100644 static/scripts/jquery.hoverIntent.js diff --git a/lib/galaxy/tools/parameters.py b/lib/galaxy/tools/parameters.py index 829ab97771b..051f51fe839 100644 --- a/lib/galaxy/tools/parameters.py +++ b/lib/galaxy/tools/parameters.py @@ -422,6 +422,7 @@ class SelectToolParameter( ToolParameter ): self.select_options = dynamic_options.DynamicOptions( select_options ) else: self.select_options = None + self.is_dynamic = ( ( self.dynamic_options is not None ) or ( self.select_options is not None ) ) def get_options( self, trans, other_values ): if self.select_options: func = '''self.select_options.%s( trans, other_values )''' %self.select_options.func @@ -441,6 +442,17 @@ class SelectToolParameter( ToolParameter ): else: return self.legal_values def get_html_field( self, trans=None, value=None, other_values={} ): + # Dynamic options are not yet supported in workflow, allow + # specifying the value as text for now. + if self.is_dynamic and trans.workflow_building_mode: + if self.multiple: + if value is None: + value = "" + else: + value = "\n".join( value ) + return form_builder.TextArea( self.name, value=value ) + else: + return form_builder.TextField( self.name, value=(value or "") ) if value is not None: if not isinstance( value, list ): value = [ value ] field = form_builder.SelectField( self.name, self.multiple, self.display ) @@ -451,6 +463,8 @@ class SelectToolParameter( ToolParameter ): field.add_option( text, optval, selected ) return field def from_html( self, value, trans=None, other_values={} ): + if self.is_dynamic and trans.workflow_building_mode: + return value legal_values = self.get_legal_values( trans, other_values ) if isinstance( value, list ): if not(self.repeat): @@ -566,6 +580,7 @@ class ColumnListParameter( SelectToolParameter ): self.numerical = str_bool( elem.get( "numerical", False )) self.force_select = str_bool( elem.get( "force_select", True )) self.data_ref = elem.get( "data_ref", None ) + self.is_dynamic = True def get_column_list( self, trans, other_values ): """ Generate a select list containing the columns of the associated diff --git a/lib/galaxy/web/controllers/workflow_editor.py b/lib/galaxy/web/controllers/workflow_editor.py index d20463512a7..56377dad0d4 100644 --- a/lib/galaxy/web/controllers/workflow_editor.py +++ b/lib/galaxy/web/controllers/workflow_editor.py @@ -1,6 +1,7 @@ from galaxy.web.base.controller import * from galaxy.tools.parameters import DataToolParameter +from galaxy.tools import DefaultToolState from galaxy.datatypes.data import Data class WorkflowEditor( BaseController ): @@ -14,13 +15,21 @@ class WorkflowEditor( BaseController ): def canvas( self, trans ): return trans.fill_template( "workflow_editor/canvas.mako" ) - @web.expose - def tool_form( self, trans, tool_id ): + @web.json + def tool_form( self, trans, tool_id=None, **incoming ): + trans.workflow_building_mode = True tool = trans.app.toolbox.tools_by_id[tool_id] - return trans.fill_template( "workflow_editor/tool_form.mako", tool=tool, as_html=as_html ) + state = DefaultToolState() + state.decode( incoming.pop("tool_state"), tool, trans.app ) + errors = tool.update_state( trans, tool.inputs_by_page[state.page], state.inputs, incoming ) + rval = {} + rval['form_html'] = trans.fill_template( "workflow_editor/tool_form.mako", tool=tool, as_html=as_html, values=state.inputs ) + rval['state'] = state.encode( tool, trans.app ) + return rval @web.json def get_tool_info( self, trans, tool_id ): + trans.workflow_building_mode = True tool = trans.app.toolbox.tools_by_id[tool_id] rval = {} rval['name'] = tool.name @@ -34,9 +43,9 @@ class WorkflowEditor( BaseController ): for name, ( format, metadata_source, parent ) in tool.outputs.iteritems(): data_outputs.append( dict( name=name, extension=format ) ) rval['data_outputs'] = data_outputs - rval['form_html'] = trans.fill_template( "workflow_editor/tool_form.mako", tool=tool, as_html=as_html ) - rval['state'] = tool.new_state( None ).encode( tool, trans.app ) - import time; time.sleep( 1 ) + state = tool.new_state( None ) + rval['form_html'] = trans.fill_template( "workflow_editor/tool_form.mako", tool=tool, as_html=as_html, values=state.inputs ) + rval['state'] = state.encode( tool, trans.app ) return rval @web.json @@ -60,8 +69,8 @@ class WorkflowEditor( BaseController ): class_to_classes[ n ] = dict( ( t, True ) for t in types ) return dict( ext_to_class_name=ext_to_class_name, class_to_classes=class_to_classes ) -def as_html( param, trans ): +def as_html( param, value, trans, prefix ): if type( param ) is DataToolParameter: return "Data input '" + param.name + "' (" + ( " or ".join( param.extensions ) ) + ")" else: - return param.get_html( trans ) \ No newline at end of file + return param.get_html_field( trans, value ).get_html( prefix ) \ No newline at end of file diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index c2a7b1a4919..f50faa14464 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -68,6 +68,10 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): base.DefaultWebTransaction.__init__( self, environ ) self.app.model.context.current.clear() self.debug = asbool( self.app.config.get( 'debug', False ) ) + # Flag indicating whether we are in workflow building mode (means + # that the current history should not be used for parameter values + # and such). + self.workflow_building_mode = False def log_event( self, message, tool_id=None, **kwargs ): """ diff --git a/static/scripts/galaxy.workflow_editor.canvas.js b/static/scripts/galaxy.workflow_editor.canvas.js index 483ff73bfde..d02a5184ae8 100644 --- a/static/scripts/galaxy.workflow_editor.canvas.js +++ b/static/scripts/galaxy.workflow_editor.canvas.js @@ -217,6 +217,42 @@ $.extend( Node.prototype, { }, make_inactive : function () { $(this.element).removeClass( "toolForm-active" ); + }, + update_field_data : function ( data ) { + var f = this.element; + this.form_html = data.form_html + this.tool_state = data.state + var node = this; + b = f.find( ".toolFormBody" ); + b.find( "div" ).remove(); + $.each( data.data_inputs, function( i, input ) { + t = $("
") + node.enable_input_terminal( t, input.extensions ); + b.append( $("
" + input.name + "
" ).prepend( t ) ); + }); + if ( ( data.data_inputs.length > 0 ) && ( data.data_outputs.length > 0 ) ) { + b.append( $( "
" ) ); + } + $.each( data.data_outputs, function( i, output ) { + var t = $( "
" ); + node.enable_output_terminal( t, output.extension ); + b.append( $("
" + output.name + "
" ).append( t ) ); + }); + if ( active_node == this ) { + // Reactive with new form_html + activate_node( this ); + } + }, + error : function ( text ) { + var b = $(this.element).find( ".toolFormBody" ); + b.find( "div" ).remove(); + var tmp = "
" + text + "
"; + this.form_html = tmp; + b.html( tmp ); + if ( active_node == this ) { + // Reactive with new form_html + activate_node( this ); + } } } ); @@ -244,7 +280,7 @@ function clear_active_node() { function activate_node( node ) { clear_active_node(); - parent.show_form_for_tool( node.form_html ); + parent.show_form_for_tool( node.form_html, node ); node.make_active(); active_node = node; } @@ -278,10 +314,10 @@ function prebuild_node_for_tool( title ) { // Make draggable $(f).draggable( { cursor: 'move', - handle: title, + // handle: title, scroll: true, - scrollSensitivity: 20, - scrollSpeed: 50, + scrollSensitivity: 10, + scrollSpeed: 20, containment: $("#shim"), // grow: true, click: function() { @@ -310,31 +346,6 @@ function prebuild_node_for_tool( title ) { return node; } -function update_node_for_tool( node, data ) { - var f = node.element; - node.form_html = data.form_html - b = f.find( ".toolFormBody" ); - b.find( "div" ).remove(); - $.each( data.data_inputs, function( i, input ) { - t = $("
") - node.enable_input_terminal( t, input.extensions ); - b.append( $("
" + input.name + "
" ).prepend( t ) ); - }); - if ( ( data.data_inputs.length > 0 ) && ( data.data_outputs.length > 0 ) ) { - b.append( $( "
" ) ); - } - $.each( data.data_outputs, function( i, output ) { - var t = $( "
" ); - node.enable_output_terminal( t, output.extension ); - b.append( $("
" + output.name + "
" ).append( t ) ); - }); - if ( active_node == node ) { - // Reactive with new form_html - activate_node( node ); - } - return node; -}; - var ext_to_type = null; var type_to_type = null; diff --git a/static/scripts/jquery.form.js b/static/scripts/jquery.form.js new file mode 100644 index 00000000000..927e10fe3d3 --- /dev/null +++ b/static/scripts/jquery.form.js @@ -0,0 +1,819 @@ +/* + * jQuery Form Plugin + * @requires jQuery v1.1 or later + * + * Examples at: http://malsup.com/jquery/form/ + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * Revision: $Id$ + */ + (function($) { +/** + * ajaxSubmit() provides a mechanism for submitting an HTML form using AJAX. + * + * ajaxSubmit accepts a single argument which can be either a success callback function + * or an options Object. If a function is provided it will be invoked upon successful + * completion of the submit and will be passed the response from the server. + * If an options Object is provided, the following attributes are supported: + * + * target: Identifies the element(s) in the page to be updated with the server response. + * This value may be specified as a jQuery selection string, a jQuery object, + * or a DOM element. + * default value: null + * + * url: URL to which the form data will be submitted. + * default value: value of form's 'action' attribute + * + * type: The method in which the form data should be submitted, 'GET' or 'POST'. + * default value: value of form's 'method' attribute (or 'GET' if none found) + * + * data: Additional data to add to the request, specified as key/value pairs (see $.ajax). + * + * beforeSubmit: Callback method to be invoked before the form is submitted. + * default value: null + * + * success: Callback method to be invoked after the form has been successfully submitted + * and the response has been returned from the server + * default value: null + * + * dataType: Expected dataType of the response. One of: null, 'xml', 'script', or 'json' + * default value: null + * + * semantic: Boolean flag indicating whether data must be submitted in semantic order (slower). + * default value: false + * + * resetForm: Boolean flag indicating whether the form should be reset if the submit is successful + * + * clearForm: Boolean flag indicating whether the form should be cleared if the submit is successful + * + * + * The 'beforeSubmit' callback can be provided as a hook for running pre-submit logic or for + * validating the form data. If the 'beforeSubmit' callback returns false then the form will + * not be submitted. The 'beforeSubmit' callback is invoked with three arguments: the form data + * in array format, the jQuery object, and the options object passed into ajaxSubmit. + * The form data array takes the following form: + * + * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ] + * + * If a 'success' callback method is provided it is invoked after the response has been returned + * from the server. It is passed the responseText or responseXML value (depending on dataType). + * See jQuery.ajax for further details. + * + * + * The dataType option provides a means for specifying how the server response should be handled. + * This maps directly to the jQuery.httpData method. The following values are supported: + * + * 'xml': if dataType == 'xml' the server response is treated as XML and the 'success' + * callback method, if specified, will be passed the responseXML value + * 'json': if dataType == 'json' the server response will be evaluted and passed to + * the 'success' callback, if specified + * 'script': if dataType == 'script' the server response is evaluated in the global context + * + * + * Note that it does not make sense to use both the 'target' and 'dataType' options. If both + * are provided the target will be ignored. + * + * The semantic argument can be used to force form serialization in semantic order. + * This is normally true anyway, unless the form contains input elements of type='image'. + * If your form must be submitted with name/value pairs in semantic order and your form + * contains an input of type='image" then pass true for this arg, otherwise pass false + * (or nothing) to avoid the overhead for this logic. + * + * + * When used on its own, ajaxSubmit() is typically bound to a form's submit event like this: + * + * $("#form-id").submit(function() { + * $(this).ajaxSubmit(options); + * return false; // cancel conventional submit + * }); + * + * When using ajaxForm(), however, this is done for you. + * + * @example + * $('#myForm').ajaxSubmit(function(data) { + * alert('Form submit succeeded! Server returned: ' + data); + * }); + * @desc Submit form and alert server response + * + * + * @example + * var options = { + * target: '#myTargetDiv' + * }; + * $('#myForm').ajaxSubmit(options); + * @desc Submit form and update page element with server response + * + * + * @example + * var options = { + * success: function(responseText) { + * alert(responseText); + * } + * }; + * $('#myForm').ajaxSubmit(options); + * @desc Submit form and alert the server response + * + * + * @example + * var options = { + * beforeSubmit: function(formArray, jqForm) { + * if (formArray.length == 0) { + * alert('Please enter data.'); + * return false; + * } + * } + * }; + * $('#myForm').ajaxSubmit(options); + * @desc Pre-submit validation which aborts the submit operation if form data is empty + * + * + * @example + * var options = { + * url: myJsonUrl.php, + * dataType: 'json', + * success: function(data) { + * // 'data' is an object representing the the evaluated json data + * } + * }; + * $('#myForm').ajaxSubmit(options); + * @desc json data returned and evaluated + * + * + * @example + * var options = { + * url: myXmlUrl.php, + * dataType: 'xml', + * success: function(responseXML) { + * // responseXML is XML document object + * var data = $('myElement', responseXML).text(); + * } + * }; + * $('#myForm').ajaxSubmit(options); + * @desc XML data returned from server + * + * + * @example + * var options = { + * resetForm: true + * }; + * $('#myForm').ajaxSubmit(options); + * @desc submit form and reset it if successful + * + * @example + * $('#myForm).submit(function() { + * $(this).ajaxSubmit(); + * return false; + * }); + * @desc Bind form's submit event to use ajaxSubmit + * + * + * @name ajaxSubmit + * @type jQuery + * @param options object literal containing options which control the form submission process + * @cat Plugins/Form + * @return jQuery + */ +$.fn.ajaxSubmit = function(options) { + if (typeof options == 'function') + options = { success: options }; + + options = $.extend({ + url: this.attr('action') || window.location, + type: this.attr('method') || 'GET' + }, options || {}); + + // hook for manipulating the form data before it is extracted; + // convenient for use with rich editors like tinyMCE or FCKEditor + var veto = {}; + $.event.trigger('form.pre.serialize', [this, options, veto]); + if (veto.veto) return this; + + var a = this.formToArray(options.semantic); + if (options.data) { + for (var n in options.data) + a.push( { name: n, value: options.data[n] } ); + } + + // give pre-submit callback an opportunity to abort the submit + if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) return this; + + // fire vetoable 'validate' event + $.event.trigger('form.submit.validate', [a, this, options, veto]); + if (veto.veto) return this; + + var q = $.param(a);//.replace(/%20/g,'+'); + + if (options.type.toUpperCase() == 'GET') { + options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q; + options.data = null; // data is null for 'get' + } + else + options.data = q; // data is the query string for 'post' + + var $form = this, callbacks = []; + if (options.resetForm) callbacks.push(function() { $form.resetForm(); }); + if (options.clearForm) callbacks.push(function() { $form.clearForm(); }); + + // perform a load on the target only if dataType is not provided + if (!options.dataType && options.target) { + var oldSuccess = options.success || function(){}; + callbacks.push(function(data) { + if (this.evalScripts) + $(options.target).attr("innerHTML", data).evalScripts().each(oldSuccess, arguments); + else // jQuery v1.1.4 + $(options.target).html(data).each(oldSuccess, arguments); + }); + } + else if (options.success) + callbacks.push(options.success); + + options.success = function(data, status) { + for (var i=0, max=callbacks.length; i < max; i++) + callbacks[i](data, status, $form); + }; + + // are there files to upload? + var files = $('input:file', this).fieldValue(); + var found = false; + for (var j=0; j < files.length; j++) + if (files[j]) + found = true; + + if (options.iframe || found) // options.iframe allows user to force iframe mode + fileUpload(); + else + $.ajax(options); + + // fire 'notify' event + $.event.trigger('form.submit.notify', [this, options]); + return this; + + + // private function for handling file uploads (hat tip to YAHOO!) + function fileUpload() { + var form = $form[0]; + var opts = $.extend({}, $.ajaxSettings, options); + + var id = 'jqFormIO' + $.fn.ajaxSubmit.counter++; + var $io = $('