diff --git a/client/src/components/Workflow/Editor/Node.vue b/client/src/components/Workflow/Editor/Node.vue index 115f51317ee..eda35629761 100644 --- a/client/src/components/Workflow/Editor/Node.vue +++ b/client/src/components/Workflow/Editor/Node.vue @@ -70,6 +70,7 @@ v-for="output in outputs" :key="output.name" :output="output" + :post-job-actions="postJobActions" :get-node="getNode" :get-manager="getManager" @onAdd="onAddOutput" diff --git a/client/src/components/Workflow/Editor/NodeInput.vue b/client/src/components/Workflow/Editor/NodeInput.vue index eabe07a0f26..12950a113f7 100644 --- a/client/src/components/Workflow/Editor/NodeInput.vue +++ b/client/src/components/Workflow/Editor/NodeInput.vue @@ -61,10 +61,11 @@ export default { oldTerminal.destroyInvalidConnections(); } else { // create new terminal, connect like old terminal, destroy old terminal + // this might be a little buggy, the terminals and connectors should be vue components this.$emit("onRemove", this.input); const newTerminal = this.createTerminal(newInput); newTerminal.connectors = this.terminal.connectors.map((c) => { - return new Connector(this.getManager(), c.outputHandle, newTerminal); + return new Connector(this.getManager().canvasManager, c.outputHandle, newTerminal); }); newTerminal.destroyInvalidConnections(); this.terminal = newTerminal; diff --git a/client/src/components/Workflow/Editor/NodeOutput.vue b/client/src/components/Workflow/Editor/NodeOutput.vue index cfa11eb3490..0a28967de1a 100644 --- a/client/src/components/Workflow/Editor/NodeOutput.vue +++ b/client/src/components/Workflow/Editor/NodeOutput.vue @@ -34,6 +34,10 @@ export default { type: Function, required: true, }, + postJobActions: { + type: Object, + required: true, + }, }, data() { return { @@ -46,12 +50,8 @@ export default { return `node-${node.id}-output-${this.output.name}`; }, label() { - let extensions = this.output.extensions || this.output.type || "unspecified"; - if (Array.isArray(extensions)) { - extensions = extensions.join(", "); - } const activeLabel = this.output.activeLabel || this.output.label || this.output.name; - return `${activeLabel} (${extensions})`; + return `${activeLabel} (${this.extensions.join(", ")})`; }, activeClass() { return this.output.activeOutput && "mark-terminal-active"; @@ -67,6 +67,19 @@ export default { } return cls; }, + forcedExtension() { + const changeDatatype = + this.postJobActions[`ChangeDatatypeAction${this.output.label}`] || + this.postJobActions[`ChangeDatatypeAction${this.output.name}`]; + return changeDatatype?.action_arguments.newtype; + }, + extensions() { + let extensions = this.forcedExtension || this.output.extensions || this.output.type || "unspecified"; + if (!Array.isArray(extensions)) { + extensions = [extensions]; + } + return extensions; + }, }, watch: { label() { @@ -78,14 +91,15 @@ export default { output(newOutput) { const oldTerminal = this.terminal; if (oldTerminal instanceof this.terminalClassForOutput(newOutput)) { - oldTerminal.update(newOutput); + oldTerminal.update({ ...newOutput, extensions: this.extensions }); oldTerminal.destroyInvalidConnections(); } else { // create new terminal, connect like old terminal, destroy old terminal + // this might be a little buggy, we should replace this with proper vue components this.$emit("onRemove", this.output); this.createTerminal(newOutput); this.terminal.connectors = oldTerminal.connectors.map((c) => { - return new Connector(this.getManager(), this.terminal, c.inputHandle); + return new Connector(this.getManager().canvasManager, this.terminal, c.inputHandle); }); this.terminal.destroyInvalidConnections(); oldTerminal.destroy(); @@ -124,7 +138,7 @@ export default { ...parameters, collection_type: collection_type, collection_type_source: collection_type_source, - datatypes: output.extensions, + datatypes: this.extensions, }); } else if (output.parameter) { this.terminal = new terminalClass({ @@ -134,7 +148,7 @@ export default { } else { this.terminal = new terminalClass({ ...parameters, - datatypes: output.extensions, + datatypes: this.extensions, }); } new OutputDragging(this.getManager(), { diff --git a/client/src/components/Workflow/Editor/modules/connector.js b/client/src/components/Workflow/Editor/modules/connector.js index 1c6387afc0f..38a673d4e90 100644 --- a/client/src/components/Workflow/Editor/modules/connector.js +++ b/client/src/components/Workflow/Editor/modules/connector.js @@ -1,4 +1,3 @@ -import $ from "jquery"; import * as d3 from "d3"; import { Toast } from "ui/toast"; @@ -13,9 +12,17 @@ const ribbonOuterSingle = 6; const ribbonInnerMultiple = 1; const ribbonOuterMultiple = 3; +function offset(element) { + const rect = element.getBoundingClientRect(); + return { + top: rect.top + window.scrollY, + left: rect.left + window.scrollX, + }; +} + class Connector { - constructor(manager = {}, outputHandle = null, inputHandle = null) { - this.manager = manager; + constructor(canvasManager = {}, outputHandle = null, inputHandle = null) { + this.manager = canvasManager; this.dragging = false; this.innerClass = "ribbon-inner"; this.canvas = document.createElement("div"); @@ -64,26 +71,28 @@ class Connector { if (!outputHandle || !inputHandle) { return; } - const canvas_container = $("#canvas-container"); - const canvasZoom = this.manager.canvasZoom; if (this.dragging) { this.canvas.style.zIndex = zIndex; } - const relativeLeft = (e) => ($(e).offset().left - canvas_container.offset().left) / canvasZoom; - const relativeTop = (e) => ($(e).offset().top - canvas_container.offset().top) / canvasZoom; if (!outputHandle || !inputHandle) { return; } - // Set handle ids, used in test cases this.canvas.setAttribute("output-handle-id", outputHandle.element.getAttribute("id")); this.canvas.setAttribute("input-handle-id", inputHandle.element.getAttribute("id")); // Find the position of each handle - let start_x = relativeLeft(outputHandle.element) + handleMarginX; - let start_y = relativeTop(outputHandle.element) + 0.5 * $(outputHandle.element).height(); - let end_x = relativeLeft(inputHandle.element) + handleMarginX; - let end_y = relativeTop(inputHandle.element) + 0.5 * $(inputHandle.element).height(); + const canvasContainer = document.getElementById("canvas-container"); + const canvasZoom = this.manager.canvasZoom; + + const relativeLeft = (e) => (offset(e).left - offset(canvasContainer).left) / canvasZoom + handleMarginX; + const relativeTop = (e) => + (offset(e).top - offset(canvasContainer).top) / canvasZoom + 0.5 * inputHandle.element.offsetHeight; + + let start_x = relativeLeft(outputHandle.element); + let start_y = relativeTop(outputHandle.element); + let end_x = relativeLeft(inputHandle.element); + let end_y = relativeTop(inputHandle.element); // Calculate canvas area const canvas_min_x = Math.min(start_x, end_x); diff --git a/client/src/components/Workflow/Editor/modules/terminals.js b/client/src/components/Workflow/Editor/modules/terminals.js index 359c1c736bb..918c4ed963c 100644 --- a/client/src/components/Workflow/Editor/modules/terminals.js +++ b/client/src/components/Workflow/Editor/modules/terminals.js @@ -333,34 +333,37 @@ class BaseInputTerminal extends Terminal { } _producesAcceptableDatatype(other) { // other is a non-collection output... - for (var t in this.datatypes) { - var thisDatatype = this.datatypes[t]; + for (const t in this.datatypes) { + const thisDatatype = this.datatypes[t]; if (thisDatatype == "input") { return new ConnectionAcceptable(true, null); } - var cat_outputs = []; - const other_datatypes = other.force_datatype ? [other.force_datatype] : other.datatypes; - cat_outputs = cat_outputs.concat(other_datatypes); // FIXME: No idea what to do about case when datatype is 'input' - for (var other_datatype_i in cat_outputs) { - var other_datatype = cat_outputs[other_datatype_i]; - if ( + const validMatch = other.datatypes.some( + (other_datatype) => other_datatype == "input" || other_datatype == "_sniff_" || - this._isSubType(cat_outputs[other_datatype_i], thisDatatype) - ) { - return new ConnectionAcceptable(true, null); - } else if (!this.datatypesMapper.datatypes.includes(other_datatype)) { - return new ConnectionAcceptable( - false, - `Effective output data type [${other_datatype}] unknown. This tool cannot be executed on this Galaxy Server at this moment, please contact the Administrator.` - ); - } + this._isSubType(other_datatype, thisDatatype) + ); + if (validMatch) { + return new ConnectionAcceptable(true, null); } } + const datatypesSet = new Set(this.datatypesMapper.datatypes); + const invalidDatatypes = other.datatypes.filter((datatype) => !datatypesSet.has(datatype)); + if (invalidDatatypes.length) { + return new ConnectionAcceptable( + false, + `Effective output data type(s) [${invalidDatatypes.join( + ", " + )}] unknown. This tool cannot be executed on this Galaxy Server at this moment, please contact the Administrator.` + ); + } return new ConnectionAcceptable( false, - `Effective output data type(s) [${cat_outputs}] do not appear to match input type(s) [${this.datatypes}].` + `Effective output data type(s) [${other.datatypes.join( + ", " + )}] do not appear to match input type(s) [${this.datatypes.join(", ")}].` ); } _isSubType(child, parent) { @@ -606,10 +609,6 @@ class BaseOutputTerminal extends Terminal { this.setMapOver(this.node.mapOver); } } - get force_datatype() { - const changeOutputDatatype = this.node.postJobActions["ChangeDatatypeAction" + this.name]; - return changeOutputDatatype ? changeOutputDatatype.action_arguments["newtype"] : null; - } update(output) { this.datatypes = output.datatypes || output.extensions; this.optional = output.optional; diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml index 6ac9c5d544f..09fac4407a9 100644 --- a/client/src/utils/navigation/navigation.yml +++ b/client/src/utils/navigation/navigation.yml @@ -576,6 +576,10 @@ workflow_editor: destroy: '${_} .node-destroy' clone: '${_} .node-clone' + + output_data_row: + type: xpath + selector: '//div[contains(@class, "output-data-row") and contains(string(), "${output_name} (${extension})")]' output_terminal: "${_} [output-name='${name}']" input_terminal: "${_} [input-name='${name}']" diff --git a/client/tests/qunit/tests/workflow_editor_tests.js b/client/tests/qunit/tests/workflow_editor_tests.js index bf4f43b7317..7d67f2282d1 100644 --- a/client/tests/qunit/tests/workflow_editor_tests.js +++ b/client/tests/qunit/tests/workflow_editor_tests.js @@ -148,7 +148,7 @@ QUnit.test("test destroy", function (assert) { }); QUnit.test("can accept exact datatype", function (assert) { - const other = { node: {}, datatypes: ["txt"], force_datatype: null }; // input also txt + const other = { node: {}, datatypes: ["txt"] }; // input also txt assert.ok(this.test_accept(other)); }); @@ -162,16 +162,6 @@ QUnit.test("cannot accept incorrect datatype", function (assert) { assert.ok(!this.test_accept(other)); }); -QUnit.test("can accept incorrect datatype if converted with PJA", function (assert) { - const other = { node: {}, datatypes: ["binary"], force_datatype: "txt", name: "out1" }; // Was binary but converted to txt - assert.ok(this.test_accept(other)); -}); - -QUnit.test("cannot accept incorrect datatype if converted with PJA to incompatible type", function (assert) { - const other = { node: {}, datatypes: ["binary"], force_datatype: "bam", name: "out1" }; - assert.ok(!this.test_accept(other)); -}); - QUnit.test("can accept inputs", function (assert) { // Other is data input module - always accept (currently - could be // more intelligent by looking at what else input is connected to. @@ -324,11 +314,11 @@ QUnit.module("Input collection terminal model test", { QUnit.test("Collection output can connect to same collection input type", function (assert) { const inputTerminal = this.input_terminal; const outputTerminal = new Terminals.OutputCollectionTerminal({ - datatypes: "txt", + datatypes: ["txt"], collection_type: "list", node: {}, }); - outputTerminal.node = {postJobActions: {}}; + outputTerminal.node = { postJobActions: {} }; assert.ok( inputTerminal.canAccept(outputTerminal).canAccept, "Input terminal " + inputTerminal + " can not accept " + outputTerminal @@ -338,7 +328,7 @@ QUnit.test("Collection output can connect to same collection input type", functi QUnit.test("Optional collection output can not connect to required collection input", function (assert) { const inputTerminal = this.input_terminal; const outputTerminal = new Terminals.OutputCollectionTerminal({ - datatypes: "txt", + datatypes: ["txt"], collection_type: "list", optional: true, node: {}, @@ -350,7 +340,7 @@ QUnit.test("Optional collection output can not connect to required collection in QUnit.test("Collection output cannot connect to different collection input type", function (assert) { const inputTerminal = this.input_terminal; const outputTerminal = new Terminals.OutputCollectionTerminal({ - datatypes: "txt", + datatypes: ["txt"], collection_type: "paired", node: {}, }); @@ -519,7 +509,7 @@ QUnit.test("update_field_data destroys old terminals", function (assert) { this.update_field_data_with_new_input(); Vue.nextTick(() => { assert.ok(destroy_spy.called); - }) + }); }); }); @@ -698,37 +688,38 @@ QUnit.test("replacing terminal on data input with collection changes mapping vie }); }); -QUnit.test("replacing terminal on data collection input with simple input changes mapping view type", function ( - assert -) { - const node = this.node; - node.inputs.push({ name: "TestName", extensions: ["txt"], input_type: "parameter" }); - this.connectAttachedMappedOutput((connector) => { - const connector_destroy_spy = sinon.spy(connector, "destroy"); - const data = { - inputs: [{ name: "TestName", extensions: ["txt"], input_type: "dataset" }], - outputs: [], - }; - node.setNode(data); - Vue.nextTick(() => { - $(node.element).find(".input-terminal")[0].terminal; - assert.ok(connector_destroy_spy.called); +QUnit.test( + "replacing terminal on data collection input with simple input changes mapping view type", + function (assert) { + const node = this.node; + node.inputs.push({ name: "TestName", extensions: ["txt"], input_type: "parameter" }); + this.connectAttachedMappedOutput((connector) => { + const connector_destroy_spy = sinon.spy(connector, "destroy"); + const data = { + inputs: [{ name: "TestName", extensions: ["txt"], input_type: "dataset" }], + outputs: [], + }; + node.setNode(data); + Vue.nextTick(() => { + $(node.element).find(".input-terminal")[0].terminal; + assert.ok(connector_destroy_spy.called); + }); }); - }); -}); + } +); // global InputTerminalView QUnit.module("Input terminal view", { beforeEach: function () { this.node = buildNode({ type: "tool", name: "newnode" }); - this.input = { name: "i1", extensions: "txt", multiple: false }; + this.input = { name: "i1", extensions: ["txt"], multiple: false }; this.node.initData({ inputs: [this.input], outputs: [] }); }, }); QUnit.test("terminal added to node", function (assert) { assert.ok(this.node.inputTerminals.i1); - assert.equal(this.node.inputTerminals.i1.datatypes, ["txt"]); + assert.equal(this.node.inputTerminals.i1.datatypes[0], "txt"); assert.equal(this.node.inputTerminals.i1.multiple, false); }); @@ -746,14 +737,14 @@ QUnit.test("terminal element", function (assert) { QUnit.module("Output terminal view", { beforeEach: function () { this.node = buildNode({ type: "tool", name: "newnode" }); - this.output = { name: "o1", extensions: "txt" }; + this.output = { name: "o1", extensions: ["txt"] }; this.node.initData({ inputs: [], outputs: [this.output] }); }, }); QUnit.test("terminal added to node", function (assert) { assert.ok(this.node.outputTerminals.o1); - assert.equal(this.node.outputTerminals.o1.datatypes, ["txt"]); + assert.equal(this.node.outputTerminals.o1.datatypes[0], "txt"); }); QUnit.test("terminal element", function (assert) { @@ -1022,7 +1013,7 @@ QUnit.module("terminal mapping logic", { verifyNotMappedOver: function (assert, terminal) { assert.ok(!terminal.mapOver.isCollection); }, - verifyDefaultMapOver: function(assert, terminal) { + verifyDefaultMapOver: function (assert, terminal) { const outputCollectionTerminal = this.newOutputCollectionTerminal("list"); assert.ok(!terminal.node.mapOver); const connector = new Connector({}, outputCollectionTerminal, terminal); @@ -1030,7 +1021,7 @@ QUnit.module("terminal mapping logic", { assert.ok(terminal.node.mapOver); terminal.disconnect(connector); assert.ok(!terminal.node.mapOver); - } + }, }); QUnit.test("unconstrained input can be mapped over", function (assert) { @@ -1046,14 +1037,15 @@ QUnit.test("unmapped input can be mapped over if matching connected input termin this.verifyAttachable(assert, this.inputTerminal1, "list"); }); -QUnit.test("unmapped input cannot be mapped over if not matching connected input terminals map type", function ( - assert -) { - this.inputTerminal1 = this.newInputTerminal(); - const connectedInput = this.addConnectedInput(this.inputTerminal1); - connectedInput.setMapOver(new Terminals.CollectionTypeDescription("paired")); - this.verifyNotAttachable(assert, this.inputTerminal1, "list"); -}); +QUnit.test( + "unmapped input cannot be mapped over if not matching connected input terminals map type", + function (assert) { + this.inputTerminal1 = this.newInputTerminal(); + const connectedInput = this.addConnectedInput(this.inputTerminal1); + connectedInput.setMapOver(new Terminals.CollectionTypeDescription("paired")); + this.verifyNotAttachable(assert, this.inputTerminal1, "list"); + } +); QUnit.test( "unmapped input can be attached to by output collection if matching connected input terminals map type", @@ -1137,17 +1129,18 @@ QUnit.test("unmapped input with connected mapped outputs can be mapped over if m this.verifyAttachable(assert, this.inputTerminal1, "list"); }); -QUnit.test("unmapped input with connected mapped outputs cannot be mapped over if mapover not matching", function ( - assert -) { - // It would invalidate the connections - someday maybe we could try to - // recursively map over everything down the DAG - it would be expensive - // to check that though. - this.inputTerminal1 = this.newInputTerminal(); - const connectedOutput = this.addConnectedOutput(this.inputTerminal1); - connectedOutput.setMapOver(new Terminals.CollectionTypeDescription("paired")); - this.verifyNotAttachable(assert, this.inputTerminal1, "list"); -}); +QUnit.test( + "unmapped input with connected mapped outputs cannot be mapped over if mapover not matching", + function (assert) { + // It would invalidate the connections - someday maybe we could try to + // recursively map over everything down the DAG - it would be expensive + // to check that though. + this.inputTerminal1 = this.newInputTerminal(); + const connectedOutput = this.addConnectedOutput(this.inputTerminal1); + connectedOutput.setMapOver(new Terminals.CollectionTypeDescription("paired")); + this.verifyNotAttachable(assert, this.inputTerminal1, "list"); + } +); QUnit.test("explicitly constrained input can not be mapped over by incompatible collection type", function (assert) { this.inputTerminal1 = this.newInputTerminal(); @@ -1245,15 +1238,16 @@ QUnit.test("resetMappingIfNeeded an input resets node outputs if they not connec this.verifyNotMappedOver(assert, output); }); -QUnit.test("resetMappingIfNeeded an input resets node collection outputs if they not connected to anything", function ( - assert -) { - this.inputTerminal1 = this.newInputTerminal("list"); - const output = this.addCollectionOutput(this.inputTerminal1); - output.setMapOver(new Terminals.CollectionTypeDescription("list")); - this.inputTerminal1.resetMappingIfNeeded(); - this.verifyNotMappedOver(assert, output); -}); +QUnit.test( + "resetMappingIfNeeded an input resets node collection outputs if they not connected to anything", + function (assert) { + this.inputTerminal1 = this.newInputTerminal("list"); + const output = this.addCollectionOutput(this.inputTerminal1); + output.setMapOver(new Terminals.CollectionTypeDescription("list")); + this.inputTerminal1.resetMappingIfNeeded(); + this.verifyNotMappedOver(assert, output); + } +); QUnit.test("resetMappingIfNeeded resets if not last mapped over input", function (assert) { // Idea here is that other nodes are forcing output to still be mapped diff --git a/lib/galaxy/managers/workflows.py b/lib/galaxy/managers/workflows.py index 596aabc22e1..f43aa4df8ea 100644 --- a/lib/galaxy/managers/workflows.py +++ b/lib/galaxy/managers/workflows.py @@ -1092,7 +1092,7 @@ class WorkflowContentsManager(UsesAnnotations): "outputs": module.get_all_outputs(), "config_form": config_form, "annotation": annotation_str, - "post_job_actions": {}, + "post_job_actions": module.get_post_job_actions({}), "uuid": str(step.uuid) if step.uuid else None, "workflow_outputs": [], } diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py index e257f820b00..635fef8dec0 100644 --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -227,7 +227,7 @@ class WorkflowModule: return self.get_all_outputs(data_only=True) def get_post_job_actions(self, incoming): - return [] + return {} def check_and_update_state(self): """ @@ -428,6 +428,10 @@ class SubWorkflowModule(WorkflowModule): _modules: Optional[List[Any]] = None subworkflow: Workflow + def __init__(self, trans, content_id=None, **kwds): + super().__init__(trans, content_id, **kwds) + self.post_job_actions: Optional[Dict[str, Any]] = None + @classmethod def from_dict(Class, trans, d, **kwds): module = super().from_dict(trans, d, **kwds) @@ -510,6 +514,7 @@ class SubWorkflowModule(WorkflowModule): def get_all_outputs(self, data_only=False): outputs = [] + self.post_job_actions = {} if hasattr(self.subworkflow, "workflow_outputs"): from galaxy.managers.workflows import WorkflowContentsManager @@ -535,7 +540,7 @@ class SubWorkflowModule(WorkflowModule): data_output["name"] == workflow_output["output_name"] or data_output_uuid == workflow_output_uuid ): - data_output["name"] = label + data_output["label"] = label # That's the right data_output break else: @@ -546,9 +551,21 @@ class SubWorkflowModule(WorkflowModule): f"Workflow output '{workflow_output['output_name']}' defined, but not listed among data outputs" ) continue + post_job_actions = step["post_job_actions"].copy() + change_datatype_action = post_job_actions.pop(f"ChangeDatatypeAction{data_output['name']}", None) + # Post job actions are referred to by tool output name, + # but that's not guaranteed to be unique within a workflow, + # but the label is unique. + if change_datatype_action: + post_job_actions[f"ChangeDatatypeAction{label}"] = change_datatype_action + + self.post_job_actions.update(post_job_actions) outputs.append(data_output) return outputs + def get_post_job_actions(self, incoming): + return self.post_job_actions + def get_content_id(self): return self.trans.security.encode_id(self.subworkflow.id) diff --git a/lib/galaxy/workflow/refactor/execute.py b/lib/galaxy/workflow/refactor/execute.py index 6a8f5839066..76681506b95 100644 --- a/lib/galaxy/workflow/refactor/execute.py +++ b/lib/galaxy/workflow/refactor/execute.py @@ -517,7 +517,7 @@ class WorkflowRefactorExecutor: # TODO: find workflow outputs that need to be dropped and report them upgrade_inputs = step.module.get_all_inputs() upgrade_outputs = step.module.get_all_outputs() - upgrade_output_names = [u["name"] for u in upgrade_outputs] + upgrade_output_names = [u.get("label") or u["name"] for u in upgrade_outputs] upgrade_order_index = step_def["id"] upgrade_label = step_def.get("label") all_input_connections = step_def.get("input_connections") diff --git a/lib/galaxy_test/selenium/test_workflow_editor.py b/lib/galaxy_test/selenium/test_workflow_editor.py index b500e9ecc4c..8a4b5cf139a 100644 --- a/lib/galaxy_test/selenium/test_workflow_editor.py +++ b/lib/galaxy_test/selenium/test_workflow_editor.py @@ -1,6 +1,7 @@ import json import yaml +from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.keys import Keys from galaxy_test.base.workflow_fixtures import ( @@ -531,6 +532,64 @@ steps: element.wait_for_and_send_keys(Keys.BACKSPACE) element.wait_for_and_send_keys(value) + @selenium_test + def test_change_datatype(self): + self.open_in_workflow_editor( + """ +class: GalaxyWorkflow +inputs: [] +steps: + - tool_id: create_2 + label: create_2 + - tool_id: checksum + label: checksum + in: + input: create_2/out_file1 +""" + ) + editor = self.components.workflow_editor + self.assert_connected("create_2#out_file1", "checksum#input") + node = editor.node._(label="create_2") + node.wait_for_and_click() + editor.configure_output(output="out_file1").wait_for_and_click() + editor.change_datatype.wait_for_and_click() + editor.select_dataype_text_search.wait_for_and_send_keys("bam") + editor.select_datatype(datatype="bam").wait_for_and_click() + editor.node.output_data_row(output_name="out_file1", extension="bam").wait_for_visible() + self.assert_not_connected("create_2#out_file1", "checksum#input") + + def test_change_datatype_in_subworkflow(self): + self.open_in_workflow_editor( + """ +class: GalaxyWorkflow +inputs: [] +steps: + nested_workflow: + run: + class: GalaxyWorkflow + inputs: [] + steps: + - tool_id: create_2 + label: create_2 + outputs: + out_file1: + change_datatype: bam + outputs: + workflow_output: + outputSource: create_2/out_file1 + metadata_bam: + tool_id: metadata_bam +""" + ) + editor = self.components.workflow_editor + node = editor.node._(label="nested_workflow") + node.wait_for_and_click() + node.output_data_row(output_name="workflow_output", extension="bam").wait_for_visible() + # Move canvas, so terminals are in viewport + self.move_center_of_canvas(xoffset=100, yoffset=100) + self.workflow_editor_connect("nested_workflow#out_file1", "metadata_bam#input_bam") + self.assert_connected("nested_workflow#out_file1", "metadata_bam#input_bam") + @selenium_test def test_editor_duplicate_node(self): workflow_id = self.workflow_populator.upload_yaml_workflow(WORKFLOW_SIMPLE_CAT_TWICE) @@ -788,3 +847,8 @@ steps: modal_element = self.components.workflow_editor.state_modal_body.wait_for_visible() text = modal_element.text assert expected_text in text, f"Failed to find expected text [{expected_text}] in modal text [{text}]" + + def move_center_of_canvas(self, xoffset=0, yoffset=0): + canvas = self.driver.find_element_by_id("canvas-container") + chains = ActionChains(self.driver) + chains.click_and_hold(canvas).move_by_offset(xoffset=xoffset, yoffset=yoffset).release().perform() diff --git a/test/unit/workflows/test_modules.py b/test/unit/workflows/test_modules.py index 77ad7802146..b72ee0c1e9e 100644 --- a/test/unit/workflows/test_modules.py +++ b/test/unit/workflows/test_modules.py @@ -244,9 +244,9 @@ def test_subworkflow_new_outputs(): outputs = subworkflow_module.get_data_outputs() assert len(outputs) == 2, len(outputs) output1, output2 = outputs - assert output1["name"] == "out1" + assert output1["label"] == "out1" assert output1["extensions"] == ["input"] - assert output2["name"] == "4:out_file1", output2["name"] + assert output2["label"] == "4:out_file1", output2["label"] class MapOverTestCase(NamedTuple):