Merge pull request #15511 from mvdbeek/dev

Merge 23.0 into dev
This commit is contained in:
Martin Cech
2023-02-06 23:56:27 +01:00
committed by GitHub
26 changed files with 208 additions and 122 deletions
+12 -4
View File
@@ -127,8 +127,19 @@ export default {
},
created() {
this.onCloneInputs();
// build flat formData that is ready to be submitted
this.formData = this.buildFormData();
// emit back to parent, so that parent has submittable data
this.$emit("onChange", this.formData);
},
methods: {
buildFormData() {
const params = {};
Object.entries(this.formIndex).forEach(([key, input]) => {
params[key] = input.value;
});
return params;
},
onReplaceParams() {
let refreshOnChange = false;
Object.entries(this.replaceParams).forEach(([key, value]) => {
@@ -159,10 +170,7 @@ export default {
},
onChange(refreshOnChange) {
this.onCreateIndex();
const params = {};
Object.entries(this.formIndex).forEach(([key, input]) => {
params[key] = input.value;
});
const params = this.buildFormData();
if (JSON.stringify(params) != JSON.stringify(this.formData)) {
this.formData = params;
this.resetError();
@@ -49,11 +49,10 @@ describe("FormElement", () => {
});
expect(wrapper.find(".ui-form-title-text").text()).toEqual("title_text");
expect(wrapper.findAll("button[title='Disable']").length).toEqual(1);
expect(wrapper.emitted().input[0][0]).toEqual("initial_value");
await wrapper.find(".ui-form-collapsible-icon").trigger("click");
expect(wrapper.emitted().input[1][0]).toEqual("collapsible_value");
expect(wrapper.emitted().input[1][1]).toEqual("input");
expect(wrapper.emitted().input[0][0]).toEqual("collapsible_value");
expect(wrapper.emitted().input[0][1]).toEqual("input");
await wrapper.setProps({
collapsedEnableText: "Enable Collapsible",
@@ -63,7 +62,7 @@ describe("FormElement", () => {
expect(wrapper.findAll("button[title='Disable Collapsible']").length).toEqual(0);
await wrapper.find(".ui-form-collapsible-icon").trigger("click");
expect(wrapper.emitted().input[2][0]).toEqual("default_value");
expect(wrapper.emitted().input[1][0]).toEqual("default_value");
expect(wrapper.findAll("button[title='Disable Collapsible']").length).toEqual(1);
expect(wrapper.findAll("button[title='Enable Collapsible']").length).toEqual(0);
});
+1 -2
View File
@@ -82,9 +82,8 @@ const collapsed = ref(false);
const collapsible = computed(() => !props.disabled && collapsibleValue.value !== undefined);
const connectable = computed(() => collapsible.value && Boolean(attrs.value["connectable"]));
// Determines to wether expand or collapse the input
// Determines whether to expand or collapse the input
{
setValue(props.value);
const valueJson = JSON.stringify(props.value);
connected.value = valueJson === JSON.stringify(connectedValue);
collapsed.value =
@@ -4,6 +4,7 @@
<CollectionElementsProvider
v-if="dsc"
:id="dsc.id"
ref="provider"
:key="dsc.id"
v-slot="{ loading, result: payload }"
:contents-url="contentsUrl"
@@ -76,6 +77,9 @@ export default {
const arr = this.selectedCollections;
return arr[arr.length - 1];
},
jobState() {
return this.dsc["job_state_summary"];
},
isRoot() {
return this.dsc == this.rootCollection;
},
@@ -93,6 +97,12 @@ export default {
this.$emit("update:selected-collections", []);
}
},
jobState: {
handler() {
this.$refs.provider.load();
},
deep: true,
},
},
methods: {
updateDsc(collection, fields) {
+1 -2
View File
@@ -5,8 +5,7 @@ export function loadWebhookMenuItems(items) {
Webhooks.load({
type: "masthead",
callback: function (webhooks) {
webhooks.each((model) => {
const webhook = model.toJSON();
webhooks.forEach((webhook) => {
if (webhook.activate) {
const obj = {
id: webhook.id,
@@ -6,7 +6,7 @@ import Webhooks from "utils/webhooks";
import ToolSourceMenuItem from "components/Tool/ToolSourceMenuItem";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
const { currentUser: user } = useCurrentUser();
const { currentUser: user } = useCurrentUser(false, true);
const props = defineProps({
id: {
@@ -28,9 +28,7 @@ const webhookDetails = ref([]);
Webhooks.load({
type: "tool-menu",
callback: (webhooks) => {
webhooks.each((model) => {
const webhook = model.toJSON();
webhooks.forEach((webhook) => {
if (webhook.activate && webhook.config.function) {
webhookDetails.value.push({
icon: `fa ${webhook.config.icon}`,
@@ -1,5 +1,7 @@
import { mount } from "@vue/test-utils";
import { getLocalVue, mockModule } from "tests/jest/helpers";
import MockAdapter from "axios-mock-adapter";
import axios from "axios";
import ToolCard from "./ToolCard";
import Vuex from "vuex";
import { userStore } from "store/userStore";
@@ -18,8 +20,12 @@ const createStore = (currentUser) => {
describe("ToolCard", () => {
let wrapper;
let axiosMock;
beforeEach(() => {
axiosMock = new MockAdapter(axios);
axiosMock.onGet(`/api/webhooks`).reply(200, []);
const store = createStore({
id: "user.id",
email: "user.email",
+1 -1
View File
@@ -66,7 +66,7 @@ function onSetError(e) {
errorText.value = e;
}
const { currentUser: user } = useCurrentUser();
const { currentUser: user } = useCurrentUser(false, true);
const hasUser = computed(() => !user.value.isAnonymous);
const versions = computed(() => props.options.versions);
@@ -30,6 +30,7 @@ describe("ToolForm", () => {
help: "help_text",
};
axiosMock.onGet(`/api/tools/tool_id/build?tool_version=version`).reply(200, toolData);
axiosMock.onGet(`/api/webhooks`).reply(200, []);
const citations = [];
axiosMock.onGet(`/api/tools/tool_id/citations`).reply(200, citations);
@@ -4,8 +4,8 @@ import { useConfig } from "composables/config";
import { useCurrentUser } from "composables/user";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
const { config } = useConfig();
const { currentUser } = useCurrentUser();
const { config } = useConfig(true);
const { currentUser } = useCurrentUser(false, true);
const props = defineProps({
toolId: {
@@ -1,5 +1,7 @@
import { mount } from "@vue/test-utils";
import { getLocalVue, mockModule } from "tests/jest/helpers";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import FormTool from "./FormTool";
import MockCurrentUser from "components/providers/MockCurrentUser";
import MockConfigProvider from "components/providers/MockConfigProvider";
@@ -11,6 +13,9 @@ import { createTestingPinia } from "@pinia/testing";
const localVue = getLocalVue();
describe("FormTool", () => {
const axiosMock = new MockAdapter(axios);
axiosMock.onGet(`/api/webhooks`).reply(200, []);
function mountTarget() {
const store = new Vuex.Store({
modules: {
@@ -1,62 +1,58 @@
<template>
<CurrentUser v-slot="{ user }">
<ToolCard
v-if="hasData"
:id="configForm.id"
:user="user"
:version="configForm.version"
:title="configForm.name"
:description="configForm.description"
:options="configForm"
:message-text="messageText"
:message-variant="messageVariant"
@onChangeVersion="onChangeVersion"
@onUpdateFavorites="onUpdateFavorites">
<template v-slot:body>
<FormElement
id="__label"
:value="label"
title="Label"
help="Add a step label."
:error="uniqueErrorLabel"
@input="onLabel" />
<FormElement
id="__annotation"
:value="annotation"
title="Step Annotation"
:area="true"
help="Add an annotation or notes to this step. Annotations are available when a workflow is viewed."
@input="onAnnotation" />
<FormConditional :step="step" v-on="$listeners" />
<div class="mt-2 mb-4">
<Heading h2 separator bold size="sm"> Tool Parameters </Heading>
<FormDisplay
:id="id"
:inputs="inputs"
:errors="errors"
text-enable="Set in Advance"
text-disable="Set at Runtime"
:workflow-building-mode="true"
@onChange="onChange" />
</div>
<div class="mt-2 mb-4">
<Heading h2 separator bold size="sm"> Additional Options </Heading>
<FormSection
:id="stepId"
:node-inputs="stepInputs"
:node-outputs="stepOutputs"
:step="step"
:datatypes="datatypes"
:post-job-actions="postJobActions"
@onChange="onChangePostJobActions" />
</div>
</template>
</ToolCard>
</CurrentUser>
<ToolCard
v-if="hasData"
:id="configForm.id"
:version="configForm.version"
:title="configForm.name"
:description="configForm.description"
:options="configForm"
:message-text="messageText"
:message-variant="messageVariant"
@onChangeVersion="onChangeVersion"
@onUpdateFavorites="onUpdateFavorites">
<template v-slot:body>
<FormElement
id="__label"
:value="label"
title="Label"
help="Add a step label."
:error="uniqueErrorLabel"
@input="onLabel" />
<FormElement
id="__annotation"
:value="annotation"
title="Step Annotation"
:area="true"
help="Add an annotation or notes to this step. Annotations are available when a workflow is viewed."
@input="onAnnotation" />
<FormConditional :step="step" v-on="$listeners" />
<div class="mt-2 mb-4">
<Heading h2 separator bold size="sm"> Tool Parameters </Heading>
<FormDisplay
:id="id"
:inputs="inputs"
:errors="errors"
text-enable="Set in Advance"
text-disable="Set at Runtime"
:workflow-building-mode="true"
@onChange="onChange" />
</div>
<div class="mt-2 mb-4">
<Heading h2 separator bold size="sm"> Additional Options </Heading>
<FormSection
:id="stepId"
:node-inputs="stepInputs"
:node-outputs="stepOutputs"
:step="step"
:datatypes="datatypes"
:post-job-actions="postJobActions"
@onChange="onChangePostJobActions" />
</div>
</template>
</ToolCard>
</template>
<script>
import CurrentUser from "@/components/providers/CurrentUser";
import FormDisplay from "@/components/Form/FormDisplay.vue";
import ToolCard from "@/components/Tool/ToolCard.vue";
import FormSection from "./FormSection.vue";
@@ -71,7 +67,6 @@ import { toRef } from "vue";
export default {
components: {
CurrentUser,
FormDisplay,
ToolCard,
FormElement,
@@ -111,7 +106,7 @@ export default {
},
data() {
return {
mainValues: {},
mainValues: null,
messageText: "",
messageVariant: "success",
};
@@ -169,7 +164,7 @@ export default {
* @param { Object } values contains flat key-value pairs `prefixed-name=value`
*/
onChange(values) {
const initialRequest = Object.keys(this.mainValues).length === 0;
const initialRequest = this.mainValues === null;
this.mainValues = values;
if (!initialRequest) {
this.postChanges();
+4 -2
View File
@@ -1,7 +1,7 @@
import { computed, onMounted, inject } from "vue";
/* composable config wrapper */
export function useConfig() {
export function useConfig(fetchOnce = false) {
const store = inject("store");
const config = computed(() => store.getters["config/config"]);
@@ -9,7 +9,9 @@ export function useConfig() {
// Anytime we mount this (for now), make sure to load.
onMounted(() => {
store.dispatch("config/loadConfigs");
if (!(fetchOnce && isLoaded)) {
store.dispatch("config/loadConfigs");
}
});
return { config, isLoaded };
+3 -2
View File
@@ -6,15 +6,16 @@ import type { Store } from "vuex";
/**
* composable user store wrapper
* @param noFetch when true, the user will not be fetched from the server
* @param fetchOnce when true, the user will only be fetched from the server if it is not already in the store
* @returns currentUser computed
*/
export function useCurrentUser(noFetch: boolean | Ref<boolean> = false) {
export function useCurrentUser(noFetch: boolean | Ref<boolean> = false, fetchOnce: boolean | Ref<boolean> = false) {
// TODO: add store typing
const store = inject("store") as Store<unknown>;
const currentUser = computed(() => store.getters["user/currentUser"]);
const currentFavorites = computed(() => store.getters["user/currentFavorites"]);
onMounted(() => {
if (!unref(noFetch)) {
if (!unref(noFetch) && !(Object.keys(currentUser).length > 0) && unref(fetchOnce)) {
store.dispatch("user/loadUser");
}
});
@@ -2,14 +2,11 @@ import Webhooks from "utils/webhooks";
import Utils from "utils/utils";
export function onloadWebhooks(Galaxy) {
console.log("onloadWebhooks");
if (Galaxy.config.enable_webhooks) {
Webhooks.load({
type: "onload",
callback: function (webhooks) {
webhooks.each((model) => {
var webhook = model.toJSON();
webhooks.forEach((webhook) => {
if (webhook.activate && webhook.script) {
Utils.appendScriptStyle(webhook);
}
@@ -490,6 +490,7 @@ tool_form:
parameter_checkbox: 'div.ui-form-element[id="form-element-${parameter}"] .ui-switch'
parameter_input: 'div.ui-form-element[id="form-element-${parameter}"] .ui-input'
parameter_textarea: 'div.ui-form-element[id="form-element-${parameter}"] textarea'
repeat_insert: '[data-description="repeat insert"]'
reference: '.formatted-reference'
about: '.tool-footer'
+30 -26
View File
@@ -1,12 +1,22 @@
import axios from "axios";
import Backbone from "backbone";
import { getAppRoot } from "onload/loadConfig";
import { rethrowSimple } from "@/utils/simple-error";
import Utils from "utils/utils";
const Webhooks = Backbone.Collection.extend({
url: function () {
return `${getAppRoot()}api/webhooks`;
},
});
let webhookData = undefined;
async function getWebHookData() {
if (webhookData === undefined) {
try {
const { data } = await axios.get(`${getAppRoot()}api/webhooks`);
webhookData = data;
} catch (e) {
rethrowSimple(e);
}
}
return webhookData;
}
const WebhookView = Backbone.View.extend({
el: "#webhook-view",
@@ -18,16 +28,13 @@ const WebhookView = Backbone.View.extend({
this.$el.attr("tool_id", toolId);
this.$el.attr("tool_version", toolVersion);
const webhooks = new Webhooks();
webhooks.fetch({
success: (data) => {
if (options.type) {
data.reset(filterType(data, options.type));
}
if (data.length > 0) {
this.render(weightedRandomPick(data));
}
},
getWebHookData().then((data) => {
if (options.type) {
data.reset(filterType(data, options.type));
}
if (data.length > 0) {
this.render(weightedRandomPick(data));
}
});
},
@@ -40,21 +47,18 @@ const WebhookView = Backbone.View.extend({
});
const load = (options) => {
const webhooks = new Webhooks();
webhooks.fetch({
async: options.async !== undefined ? options.async : true,
success: (data) => {
if (options.type) {
data.reset(filterType(data, options.type));
}
options.callback(data);
},
getWebHookData().then((data) => {
let filteredData = data;
if (options.type) {
filteredData = filterType(data, options.type);
}
options.callback(filteredData);
});
};
function filterType(data, type) {
return data.models.filter((item) => {
const itype = item.get("type");
return data.filter((item) => {
const itype = item.type;
if (itype) {
return itype.indexOf(type) !== -1;
} else {
+1 -1
View File
@@ -539,7 +539,7 @@ class Data(metaclass=DataMeta):
trans.fill_template_mako(
"/dataset/large_file.mako",
truncated_data=open(dataset.file_name, "rb").read(max_peek_size),
dataset=dataset,
data=dataset,
),
headers,
)
+5
View File
@@ -59,6 +59,11 @@ List of behavior changes associated with profile versions:
- Do not strip leading and trailing whitespaces in `from_work_dir` attribute.
### 23.0
- Text parameters that are inferred to be optional (i.e the `optional` tag is not set, but the tool parameter accepts an empty string)
are set to `None` for templating in Cheetah. Older tools receive the empty string `""` as the templated value.
### Examples
A normal tool:
+3 -1
View File
@@ -356,7 +356,9 @@ class ToolEvaluator:
input, value, other_values=param_dict, compute_environment=self.compute_environment
)
else:
input_values[input.name] = InputValueWrapper(input, value, param_dict)
input_values[input.name] = InputValueWrapper(
input, value, param_dict, profile=self.tool and self.tool.profile
)
# HACK: only wrap if check_values is not false, this deals with external
# tools where the inputs don't even get passed through. These
+21 -2
View File
@@ -164,6 +164,7 @@ class ToolParameter(Dictifiable):
self.hidden = input_source.get_bool("hidden", False)
self.refresh_on_change = input_source.get_bool("refresh_on_change", False)
self.optional = input_source.parse_optional()
self.optionality_inferred = False
self.is_dynamic = False
self.label = input_source.parse_label()
self.help = input_source.parse_help()
@@ -326,7 +327,24 @@ class SimpleTextToolParameter(ToolParameter):
def __init__(self, tool, input_source):
input_source = ensure_input_source(input_source)
super().__init__(tool, input_source)
self.optional = input_source.get_bool("optional", False)
optional = input_source.get("optional", None)
if optional is not None:
optional = string_as_bool(optional)
else:
# Optionality not explicitly defined, default to False
optional = False
if self.type == "text":
# A text parameter that doesn't raise a validation error on empty string
# is considered to be optional
try:
for validator in self.validators:
validator.validate("")
optional = True
self.optionality_inferred = True
except ValueError:
pass
self.optional = optional
if self.optional:
self.value = None
else:
@@ -353,7 +371,8 @@ class TextToolParameter(SimpleTextToolParameter):
>>> p = TextToolParameter(None, XML('<param name="_name" type="text" value="default" />'))
>>> print(p.name)
_name
>>> assert sorted(p.to_dict(trans).items()) == [('area', False), ('argument', None), ('datalist', []), ('help', ''), ('hidden', False), ('is_dynamic', False), ('label', ''), ('model_class', 'TextToolParameter'), ('name', '_name'), ('optional', False), ('refresh_on_change', False), ('type', 'text'), ('value', u'default')]
>>> sorted(p.to_dict(trans).items())
[('area', False), ('argument', None), ('datalist', []), ('help', ''), ('hidden', False), ('is_dynamic', False), ('label', ''), ('model_class', 'TextToolParameter'), ('name', '_name'), ('optional', True), ('refresh_on_change', False), ('type', 'text'), ('value', 'default')]
"""
def __init__(self, tool, input_source):
+1 -1
View File
@@ -98,7 +98,7 @@ class WrappedParameters:
name=input.name,
)
else:
input_values[input.name] = InputValueWrapper(input, value, incoming)
input_values[input.name] = InputValueWrapper(input, value, incoming, tool.profile)
def make_dict_copy(from_dict):
+13 -3
View File
@@ -66,7 +66,7 @@ class ToolParameterValueWrapper:
Base class for object that Wraps a Tool Parameter and Value.
"""
value: Union[str, List[str]]
value: Optional[Union[str, List[str]]]
input: "ToolParameter"
def __bool__(self) -> bool:
@@ -120,10 +120,20 @@ class InputValueWrapper(ToolParameterValueWrapper):
def __init__(
self,
input: "ToolParameter",
value: str,
value: Optional[str],
other_values: Optional[Dict[str, str]] = None,
profile: Optional[float] = None,
) -> None:
self.input = input
if (
value is None
and input.type == "text"
and input.optional
and input.optionality_inferred
and (profile is None or profile < 23.0)
):
# Tools with old profile versions may treat an optional text parameter as `""`
value = ""
self.value = value
self._other_values: Dict[str, str] = other_values or {}
@@ -236,7 +246,7 @@ class SelectToolParameterWrapper(ToolParameterValueWrapper):
compute_environment: Optional["ComputeEnvironment"] = None,
):
self.input = input
self.value = value
self.value: Union[str, List[str]] = value
self.input.value_label = input.value_to_display_text(value)
self._other_values = other_values or {}
self.compute_environment = compute_environment
@@ -918,6 +918,26 @@ steps:
output_connector.send_keys(Keys.SPACE)
assert self.driver.switch_to.active_element.text == "No compatible input found in workflow"
@selenium_test
def test_insert_input_handling(self):
self.open_in_workflow_editor(
"""class: GalaxyWorkflow
inputs: []
steps:
build_list:
tool_id: __BUILD_LIST__
"""
)
editor = self.components.workflow_editor
node = editor.node._(label="build_list")
node.wait_for_and_click()
assert not node.has_class("input-terminal")
self.components.tool_form.repeat_insert.wait_for_and_click()
node.input_terminal(name="datasets_0|input").wait_for_present()
self.components.tool_form.repeat_insert.wait_for_and_click()
node.input_terminal(name="datasets_1|input").wait_for_present()
self.assert_workflow_has_changes_and_save()
@selenium_test
def test_workflow_output_handling(self):
self.open_in_workflow_editor(
+5 -2
View File
@@ -3,13 +3,16 @@
#set $int_val_inc = int($inttest) + 1
#set $int_flot_val_inc = int($floattest) + 1
#set $float_val_inc = float($floattest) + 1
echo $int_val_inc >> '$out_file1';
#set $text_val = $texttest.strip()
echo $int_val_inc >> '$out_file1';
echo $int_flot_val_inc >> '$out_file1';
echo $float_val_inc >> '$out_file1';
echo $text_val >> '#out_file1'
</command>
<inputs>
<param name="inttest" value="1" type="integer" />
<param name="floattest" value="1.0" type="float" />
<param name="texttest" type="text" />
</inputs>
<outputs>
<data name="out_file1" format="txt" />
@@ -27,4 +30,4 @@
</output>
</test>
</tests>
</tool>
</tool>
@@ -91,7 +91,9 @@ class TestParameterParsing(BaseParameterTestCase):
def test_parse_optional(self):
param = self._parameter_for(
xml="""
<param type="text" name="texti" value="mydefault" />
<param type="text" name="texti" value="mydefault">
<validator type="empty_field" />
</param>
"""
)
assert param.optional is False