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 = $('