From adc2d60bedb8276f287d27aab2ed63c55a7625cc Mon Sep 17 00:00:00 2001 From: Aysam Guerler Date: Tue, 13 Jan 2015 01:09:03 -0500 Subject: [PATCH] ToolForm/Workflow: Add post job action forms, enable custom tool form sections --- .../scripts/galaxy.workflow_editor.canvas.js | 19 +- .../scripts/mvc/tools/tools-form-base.js | 5 + .../scripts/mvc/tools/tools-form-workflow.js | 200 +++++++++++++++++- .../galaxy/scripts/mvc/tools/tools-section.js | 106 ++++++++-- client/galaxy/scripts/mvc/tools/tools-tree.js | 31 +-- client/galaxy/scripts/mvc/ui/ui-misc.js | 6 + client/galaxy/scripts/mvc/ui/ui-portlet.js | 28 +-- .../visualizations/charts/static/build-app.js | 6 +- lib/galaxy/tools/parameters/basic.py | 3 + .../scripts/galaxy.workflow_editor.canvas.js | 19 +- static/scripts/mvc/tools/tools-form-base.js | 5 + .../scripts/mvc/tools/tools-form-workflow.js | 200 +++++++++++++++++- static/scripts/mvc/tools/tools-section.js | 106 ++++++++-- static/scripts/mvc/tools/tools-tree.js | 31 +-- static/scripts/mvc/ui/ui-misc.js | 6 + static/scripts/mvc/ui/ui-portlet.js | 28 +-- .../packed/galaxy.workflow_editor.canvas.js | 2 +- .../packed/mvc/tools/tools-form-base.js | 2 +- .../packed/mvc/tools/tools-form-workflow.js | 2 +- .../scripts/packed/mvc/tools/tools-section.js | 2 +- static/scripts/packed/mvc/tools/tools-tree.js | 2 +- static/scripts/packed/mvc/ui/ui-misc.js | 2 +- static/scripts/packed/mvc/ui/ui-portlet.js | 2 +- static/style/blue/base.css | 13 +- static/style/src/less/base.less | 1 - static/style/src/less/ui.less | 11 + templates/webapps/galaxy/workflow/editor.mako | 3 + .../galaxy/workflow/editor_tool_form.mako | 6 - 28 files changed, 712 insertions(+), 135 deletions(-) diff --git a/client/galaxy/scripts/galaxy.workflow_editor.canvas.js b/client/galaxy/scripts/galaxy.workflow_editor.canvas.js index 918ecf0d124..a7143adb474 100644 --- a/client/galaxy/scripts/galaxy.workflow_editor.canvas.js +++ b/client/galaxy/scripts/galaxy.workflow_editor.canvas.js @@ -1089,7 +1089,11 @@ $.extend( Workflow.prototype, { if ( this.active_node != node ) { this.check_changes_in_active_form(); this.clear_active_node(); - parent.show_form_for_tool( node.form_html + node.tooltip, node ); + if (parent.__NEWTOOLFORM__) { + parent.show_form_for_tool( node.form_html, node ); + } else { + parent.show_form_for_tool( node.form_html + node.tooltip, node ); + } node.make_active(); this.active_node = node; } @@ -1099,7 +1103,11 @@ $.extend( Workflow.prototype, { if ( this.active_node == node && (!parent.__NEWTOOLFORM__ || force)) { // Reactive with new form_html this.check_changes_in_active_form(); //Force changes to be saved even on new connection (previously dumped) - parent.show_form_for_tool( node.form_html + node.tooltip, node ); + if (parent.__NEWTOOLFORM__) { + parent.show_form_for_tool( node.form_html, node ); + } else { + parent.show_form_for_tool( node.form_html + node.tooltip, node ); + } } }, layout : function () { @@ -1305,13 +1313,6 @@ function add_node( type, title_text, tool_id ) { return node; } -function update_node( data ) { - var node = workflow.active_node; - if (node) { - node.update_field_data(data, true); - } -} - var ext_to_type = null; var type_to_type = null; diff --git a/client/galaxy/scripts/mvc/tools/tools-form-base.js b/client/galaxy/scripts/mvc/tools/tools-form-base.js index 71eecb21069..9dd6bf64903 100644 --- a/client/galaxy/scripts/mvc/tools/tools-form-base.js +++ b/client/galaxy/scripts/mvc/tools/tools-form-base.js @@ -276,6 +276,11 @@ define(['utils/utils', 'utils/deferred', 'mvc/ui/ui-portlet', 'mvc/ui/ui-misc', buttons : this.buttons }); + // remove padding + if (this.options.workflow) { + this.portlet.$content.css('padding', '0px'); + } + // append message this.portlet.append(this.message.$el, true); diff --git a/client/galaxy/scripts/mvc/tools/tools-form-workflow.js b/client/galaxy/scripts/mvc/tools/tools-form-workflow.js index c9a45ee795e..8224fa8f8fa 100644 --- a/client/galaxy/scripts/mvc/tools/tools-form-workflow.js +++ b/client/galaxy/scripts/mvc/tools/tools-form-workflow.js @@ -7,8 +7,199 @@ define(['utils/utils', 'mvc/tools/tools-form-base'], // create form view var View = ToolFormBase.extend({ initialize: function(options) { + // link with node representation in workflow module + this.node = workflow.active_node; + if (!this.node) { + console.debug('FAILED - tools-form-workflow:initialize() - Node not found in workflow.'); + return; + } + + // initialize parameters this.workflow = true; - ToolFormBase.prototype.initialize.call(this, options); + this.options = options; + + // load extension + var self = this; + Utils.get({ + url : galaxy_config.root + 'api/datatypes', + cache : true, + success : function(datatypes) { + self.datatypes = datatypes; + self._makeSections(options.inputs); + ToolFormBase.prototype.initialize.call(self, options); + } + }); + }, + + /** Builds all sub sections + */ + _makeSections: function(inputs){ + // for annotation + inputs[Utils.uuid()] = { + label : 'Edit Step Attributes', + type : 'section', + inputs : [{ + label : 'Annotation / Notes', + name : 'annotation', + type : 'text', + area : true, + help : 'Add an annotation or notes to this step; annotations are available when a workflow is viewed.', + value : this.node.annotation + }] + } + + // for actions + this.post_job_actions = this.node.post_job_actions; + for (var i in this.node.output_terminals) { + inputs[Utils.uuid()] = this._makeSection(i); + } + }, + + /** Builds sub section with step actions/annotation + */ + _makeSection: function(output_id){ + // format datatypes + var extensions = []; + for (key in this.datatypes) { + extensions.push({ + 0 : this.datatypes[key], + 1 : this.datatypes[key] + }); + } + + // sort extensions + extensions.sort(function(a, b) { + return a.label > b.label ? 1 : a.label < b.label ? -1 : 0; + }); + + // add additional options + extensions.unshift({ + 0 : 'Sequences', + 1 : 'Sequences' + }); + extensions.unshift({ + 0 : 'Roadmaps', + 1 : 'Roadmaps' + }); + extensions.unshift({ + 0 : 'Leave unchanged', + 1 : 'None' + }); + + // create custom sub section + var input_config = { + label : 'Edit Step Action: \'' + output_id + '\'', + type : 'section', + inputs : [{ + action : 'RenameDatasetAction', + argument : 'newname', + label : 'Rename dataset', + type : 'text', + value : '', + ignore : '', + help : 'This action will rename the result dataset. Click here for more information.' + },{ + action : 'ChangeDatatypeAction', + argument : 'newtype', + label : 'Change datatype', + type : 'select', + ignore : 'None', + options : extensions, + help : 'This action will change the datatype of the output to the indicated value.' + },{ + action : 'TagDatasetAction', + argument : 'tags', + label : 'Tags', + type : 'text', + value : '', + ignore : '', + help : 'This action will set tags for the dataset.' + },{ + label : 'Assign columns', + type : 'section', + inputs : [{ + action : 'ColumnSetAction', + argument : 'chromCol', + label : 'Chrom column', + type : 'text', + value : '', + ignore : '' + },{ + action : 'ColumnSetAction', + argument : 'startCol', + label : 'Start column', + type : 'text', + value : '', + ignore : '' + },{ + action : 'ColumnSetAction', + argument : 'endCol', + label : 'End column', + type : 'text', + value : '', + ignore : '' + },{ + action : 'ColumnSetAction', + argument : 'strandCol', + label : 'Strand column', + type : 'text', + value : '', + ignore : '' + },{ + action : 'ColumnSetAction', + argument : 'nameCol', + label : 'Name column', + type : 'text', + value : '', + ignore : '' + }], + help : 'This action will set column assignments in the output dataset. Blank fields are ignored.' + },{ + action : 'EmailAction', + label : 'Email notification', + type : 'boolean', + value : 'false', + ignore : 'false', + help : 'This action will send an email notifying you when the job is done.' + },{ + action : 'DeleteIntermediatesAction', + label : 'Delete non-outputs', + type : 'boolean', + value : 'false', + ignore : 'false', + help : 'All non-output steps of this workflow will have datasets deleted if they are no longer being used as job inputs when the job this action is attached to is finished. You *must* be using workflow outputs (the snowflake) in your workflow for this to have any effect.' + }] + }; + + // visit input nodes and enrich by name/value pairs from server data + var self = this; + function visit (inputs) { + for (var i in inputs) { + var input = inputs[i]; + if (input.action) { + input.name = 'pja__' + output_id + '__' + input.action; + if (input.argument) { + input.name += '__' + input.argument; + } + var d = self.post_job_actions[input.action + output_id]; + if (d) { + if (input.argument) { + input.value = d.action_arguments && d.action_arguments[input.argument] || input.value; + } else { + input.value = 'true'; + } + } + } else { + if (input.inputs) { + visit(input.inputs); + } + } + } + } + visit(input_config.inputs); + + // return final configuration + return input_config; }, /** Builds a new model through api call and recreates the entire form @@ -38,15 +229,16 @@ define(['utils/utils', 'mvc/tools/tools-form-base'], type : 'GET', url : model_url, data : current_state, - success : function(node) { - parent.update_node(node); + success : function(data) { + // update node in workflow module + self.node.update_field_data(data, true); // process completed self.deferred.done(process_id); // log success console.debug('tools-form::_refreshForm() - States refreshed.'); - console.debug(node); + console.debug(data); }, error : function(response) { // process completed diff --git a/client/galaxy/scripts/mvc/tools/tools-section.js b/client/galaxy/scripts/mvc/tools/tools-section.js index 8038a403079..5f31bb2850c 100644 --- a/client/galaxy/scripts/mvc/tools/tools-section.js +++ b/client/galaxy/scripts/mvc/tools/tools-section.js @@ -1,8 +1,8 @@ /** This class creates a tool form section and populates it with input elements. It also handles repeat blocks and conditionals by recursively creating new sub sections. New input elements can be plugged in by adding cases to the switch block defined in the _createField() function. */ -define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-repeat', 'mvc/tools/tools-select-content', 'mvc/tools/tools-input'], - function(Utils, Table, Ui, Repeat, SelectContent, InputElement) { +define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/ui/ui-portlet', 'mvc/tools/tools-repeat', 'mvc/tools/tools-select-content', 'mvc/tools/tools-input'], + function(Utils, Table, Ui, Portlet, Repeat, SelectContent, InputElement) { // create form view var View = Backbone.View.extend({ @@ -36,13 +36,13 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep // load settings elements into table for (var i in this.inputs) { - this._add(this.inputs[i]); + this.add(this.inputs[i]); } }, /** Add a new input element */ - _add: function(input) { + add: function(input) { // link this var self = this; @@ -66,6 +66,10 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep case 'repeat': this._addRepeat(input_def); break; + // customized section + case 'section': + this._addSection(input_def); + break; // default single element row default: this._addRow(input_def); @@ -103,8 +107,7 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep // check if non-hidden elements exist var nonhidden = false; for (var j in case_def.inputs) { - var type = case_def.inputs[j].type; - if (type && type !== 'hidden') { + if (!case_def.inputs[j].hidden) { nonhidden = true; break; } @@ -244,6 +247,58 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep this.table.append(input_def.id); }, + /** Add a customized section + */ + _addSection: function(input_def) { + // link this + var self = this; + + // create sub section + var sub_section = new View(self.app, { + inputs : input_def.inputs, + cls : 'ui-table-plain' + }); + + // delete button + var button_visible = new Ui.ButtonIcon({ + icon : 'fa-eye-slash', + tooltip : 'Show/hide section', + cls : 'ui-button-icon-plain' + }); + + // create portlet for sub section + var portlet = new Portlet.View({ + title : input_def.label, + cls : 'ui-portlet-section', + operations : { + button_visible: button_visible + } + }); + portlet.append(sub_section.$el); + + // add event handler visibility button + var visible = false; + portlet.$content.hide(); + portlet.$header.css('cursor', 'pointer'); + portlet.$header.on('click', function() { + if (visible) { + visible = false; + portlet.$content.hide(); + button_visible.setIcon('fa-eye-slash'); + } else { + visible = true; + portlet.$content.fadeIn('fast'); + button_visible.setIcon('fa-eye'); + } + }); + + // create table row + this.table.add(portlet.$el); + + // append row to table + this.table.append(input_def.id); + }, + /** Add a single input field element */ _addRow: function(input_def) { @@ -273,6 +328,11 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep // append to table this.table.append(id); + // hide row if neccessary + if (input_def.hidden) { + this.table.get(id).hide(); + } + // return created field return field; }, @@ -316,7 +376,7 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep field = this._fieldHidden(input_def); break; - // hidden field + // hidden data field case 'hidden_data': field = this._fieldHidden(input_def); break; @@ -465,9 +525,9 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep _fieldText : function(input_def) { var self = this; return new Ui.Input({ - id : 'field-' + input_def.id, - area : input_def.area, - onchange: function() { + id : 'field-' + input_def.id, + area : input_def.area, + onchange : function() { self.app.trigger('refresh'); } }); @@ -476,11 +536,15 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep /** Slider field */ _fieldSlider: function(input_def) { + var self = this; return new Ui.Slider.View({ - id : 'field-' + input_def.id, - precise : input_def.type == 'float', - min : input_def.min, - max : input_def.max + id : 'field-' + input_def.id, + precise : input_def.type == 'float', + min : input_def.min, + max : input_def.max, + onchange : function() { + self.app.trigger('refresh'); + } }); }, @@ -488,18 +552,22 @@ define(['utils/utils', 'mvc/ui/ui-table', 'mvc/ui/ui-misc', 'mvc/tools/tools-rep */ _fieldHidden : function(input_def) { return new Ui.Hidden({ - id : 'field-' + input_def.id, - info : input_def.info + id : 'field-' + input_def.id, + info : input_def.info }); }, /** Boolean field */ _fieldBoolean : function(input_def) { + var self = this; return new Ui.RadioButton.View({ - id : 'field-' + input_def.id, - data : [ { label : 'Yes', value : 'true' }, - { label : 'No', value : 'false' }] + id : 'field-' + input_def.id, + data : [ { label : 'Yes', value : 'true' }, + { label : 'No', value : 'false' }], + onchange : function() { + self.app.trigger('refresh'); + } }); } }); diff --git a/client/galaxy/scripts/mvc/tools/tools-tree.js b/client/galaxy/scripts/mvc/tools/tools-tree.js index 3a8cde64e78..3525622a987 100644 --- a/client/galaxy/scripts/mvc/tools/tools-tree.js +++ b/client/galaxy/scripts/mvc/tools/tools-tree.js @@ -108,22 +108,29 @@ return Backbone.Model.extend({ convert(job_input_id, head[input.id + '-section-' + selectedCase]); } break; + // handle custom sub sections + case 'section': + convert('', node); + break; default: // get field var field = self.app.field_list[input.id]; - - // get and patch field value - var value = field.value(); - if (patch[input.type]) { - value = patch[input.type](value); - } - - // handle default value - if (!field.skip) { - if (field.validate && !field.validate(value)) { - value = null; + if (field && field.value) { + // get and patch field value + var value = field.value(); + if (patch[input.type]) { + value = patch[input.type](value); + } + + // handle default value + if (!field.skip) { + if (field.validate && !field.validate(value)) { + value = null; + } + if (input.ignore === undefined || (value && input.ignore != value)) { + add (job_input_id, input.id, value); + } } - add (job_input_id, input.id, value); } } } diff --git a/client/galaxy/scripts/mvc/ui/ui-misc.js b/client/galaxy/scripts/mvc/ui/ui-misc.js index efedef2c0c3..83678415490 100644 --- a/client/galaxy/scripts/mvc/ui/ui-misc.js +++ b/client/galaxy/scripts/mvc/ui/ui-misc.js @@ -193,6 +193,12 @@ var ButtonIcon = Backbone.View.extend({ this.disabled = false; }, + // change icon + setIcon: function(icon_cls) { + this.$('i').removeClass(this.options.icon).addClass(icon_cls); + this.options.icon = icon_cls; + }, + // template _template: function(options) { // width diff --git a/client/galaxy/scripts/mvc/ui/ui-portlet.js b/client/galaxy/scripts/mvc/ui/ui-portlet.js index 0dfcbf053be..138113ecec9 100644 --- a/client/galaxy/scripts/mvc/ui/ui-portlet.js +++ b/client/galaxy/scripts/mvc/ui/ui-portlet.js @@ -25,6 +25,7 @@ var View = Backbone.View.extend({ $content : null, $buttons : null, $operations : null, + $header : null, // initialize initialize : function(options) { @@ -35,13 +36,16 @@ var View = Backbone.View.extend({ this.setElement(this._template(this.options)); // link content - this.$content = this.$el.find('#content'); + this.$content = this.$el.find('.content'); // link title - this.$title = this.$el.find('#portlet-title-text'); + this.$title = this.$el.find('.portlet-title-text'); + + // link header + this.$header = this.$el.find('.portlet-header'); // set content format - var $portlet_content = this.$el.find('#portlet-content'); + var $portlet_content = this.$el.find('.portlet-content'); if (!this.options.scrollable) { if (this.options.title) { $portlet_content.addClass('no-scroll'); @@ -59,7 +63,7 @@ var View = Backbone.View.extend({ } // append buttons - this.$buttons = $(this.el).find('#buttons'); + this.$buttons = $(this.el).find('.buttons'); if (this.options.buttons) { // link functions var self = this; @@ -72,7 +76,7 @@ var View = Backbone.View.extend({ } // append operations - this.$operations = $(this.el).find('#operations'); + this.$operations = $(this.el).find('.operations'); if (this.options.operations) { // link functions var self = this; @@ -161,27 +165,27 @@ var View = Backbone.View.extend({ var tmpl = '
'; if (options.title) { - tmpl += '
' + - '
' + + tmpl += '
' + + '
' + '
'; if (options.icon) tmpl += ' '; - tmpl += '' + options.title + '' + + tmpl += '' + options.title + '' + '
' + '
'; } - tmpl += '
'; + tmpl += '
'; if (options.placement == 'top') { - tmpl += '
'; + tmpl += '
'; } - tmpl += '
'; + tmpl += '
'; if (options.placement == 'bottom') { - tmpl += '
'; + tmpl += '
'; } tmpl += '
' + diff --git a/config/plugins/visualizations/charts/static/build-app.js b/config/plugins/visualizations/charts/static/build-app.js index 6674660152c..9a4600e3a1d 100644 --- a/config/plugins/visualizations/charts/static/build-app.js +++ b/config/plugins/visualizations/charts/static/build-app.js @@ -1,6 +1,6 @@ -// Underscore.js 1.4.4 +// Underscore.js 1.7.0 // http://underscorejs.org -// (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. +// (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // Underscore may be freely distributed under the MIT license. -define("mvc/ui/ui-modal",[],function(){var e=Backbone.View.extend({elMain:"body",optionsDefault:{title:"ui-modal",body:"",backdrop:!0,height:null,width:null,closing_events:!1,closing_callback:null},buttonList:{},initialize:function(e){e&&this._create(e)},show:function(e){this.initialize(e),this.options.height?(this.$body.css("height",this.options.height),this.$body.css("overflow","hidden")):this.$body.css("max-height",$(window).height()/2),this.options.width&&this.$dialog.css("width",this.options.width),this.visible?this.$el.show():this.$el.fadeIn("fast"),this.visible=!0},hide:function(){this.visible=!1,this.$el.fadeOut("fast"),this.options.closing_callback&&this.options.closing_callback()},enableButton:function(e){var t=this.buttonList[e];this.$buttons.find("#"+t).prop("disabled",!1)},disableButton:function(e){var t=this.buttonList[e];this.$buttons.find("#"+t).prop("disabled",!0)},showButton:function(e){var t=this.buttonList[e];this.$buttons.find("#"+t).show()},hideButton:function(e){var t=this.buttonList[e];this.$buttons.find("#"+t).hide()},getButton:function(e){var t=this.buttonList[e];return this.$buttons.find("#"+t)},scrollTop:function(){return this.$body.scrollTop()},_create:function(e){var t=this;this.options=_.defaults(e,this.optionsDefault),this.options.body=="progress"&&(this.options.body=$('
')),this.$el&&(this.$el.remove(),$(document).off("keyup.ui-modal")),this.setElement(this._template(this.options.title)),this.$dialog=this.$el.find(".modal-dialog"),this.$body=this.$el.find(".modal-body"),this.$footer=this.$el.find(".modal-footer"),this.$buttons=this.$el.find(".buttons"),this.$backdrop=this.$el.find(".modal-backdrop"),this.$body.html(this.options.body),this.options.backdrop||this.$backdrop.removeClass("in");if(this.options.buttons){this.buttonList={};var n=0;$.each(this.options.buttons,function(e,r){var i="button-"+n++;t.$buttons.append($('').text(e).click(r)).append(" "),t.buttonList[e]=i})}else this.$footer.hide();$(this.elMain).append($(this.el)),this.options.closing_events&&($(document).on("keyup.ui-modal",function(e){e.keyCode==27&&t.hide()}),this.$el.find(".modal-backdrop").on("click",function(){t.hide()}))},_template:function(e){return'