mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #14473 from mvdbeek/change_datatype_problems
[22.05] Fix effective datatype in workflow editor node terminals
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(), {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}']"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": [],
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user