diff --git a/client/src/components/ActivityBar/ActivityBar.test.js b/client/src/components/ActivityBar/ActivityBar.test.js index 9f3e7e9b12b9..7dce1ce3fb29 100644 --- a/client/src/components/ActivityBar/ActivityBar.test.js +++ b/client/src/components/ActivityBar/ActivityBar.test.js @@ -1,7 +1,7 @@ import { createTestingPinia } from "@pinia/testing"; import { shallowMount } from "@vue/test-utils"; import { PiniaVuePlugin } from "pinia"; -import { getLocalVue } from "tests/jest/helpers"; +import { dispatchEvent, getLocalVue } from "tests/jest/helpers"; import { useConfig } from "@/composables/config"; import { useActivityStore } from "@/stores/activityStore"; @@ -37,12 +37,6 @@ function testActivity(id, newOptions = {}) { return { ...defaultOptions, ...newOptions }; } -const createBubbledEvent = (type, props = {}) => { - const event = new Event(type, { bubbles: true }); - Object.assign(event, props); - return event; -}; - describe("ActivityBar", () => { let activityStore; let eventStore; @@ -74,7 +68,7 @@ describe("ActivityBar", () => { name: "workflow-name", }); const bar = wrapper.find("[data-description='activity bar']"); - bar.element.dispatchEvent(createBubbledEvent("dragenter", { clientX: 0, clientY: 0 })); + dispatchEvent(bar, "dragenter"); const emittedEvent = wrapper.emitted()["dragstart"][0][0]; expect(emittedEvent.to).toBe("/workflows/run?id=workflow-id"); }); diff --git a/client/src/components/Form/Elements/FormCheck.test.js b/client/src/components/Form/Elements/FormCheck.test.js index 59e6bf6d3dc0..99a83267b89d 100644 --- a/client/src/components/Form/Elements/FormCheck.test.js +++ b/client/src/components/Form/Elements/FormCheck.test.js @@ -24,7 +24,7 @@ describe("FormCheck", () => { const n = 3; const options = []; for (let i = 0; i < n; i++) { - options.push([`label_${i}`, `value_${i}`]); + options.push({ label: `label_${i}`, value: `value_${i}` }); } await wrapper.setProps({ options }); const inputs = wrapper.findAll("[type='checkbox']"); @@ -52,7 +52,7 @@ describe("FormCheck", () => { const emptyValues = [0, null, false, true, undefined]; const options = []; for (let i = 0; i < emptyValues.length; i++) { - options.push([`label_${i}`, emptyValues[i]]); + options.push({ label: `label_${i}`, value: emptyValues[i] }); } await wrapper.setProps({ options }); const inputs = wrapper.findAll("[type='checkbox']"); @@ -70,7 +70,7 @@ describe("FormCheck", () => { const n = 3; const options = []; for (let i = 0; i < n; i++) { - options.push([`label_${i}`, `value_${i}`]); + options.push({ label: `label_${i}`, value: `value_${i}` }); } await wrapper.setProps({ options }); const inputs = wrapper.findAll("[type='checkbox']"); @@ -84,7 +84,7 @@ describe("FormCheck", () => { await inputs.at(0).setChecked(); expect(inputs.at(0).element.checked).toBeTruthy(); /* ...confirm corresponding options checked */ - const values = options.map((option) => option[1]); + const values = options.map((option) => option.value); expect(wrapper.emitted()["input"][0][0]).toStrictEqual(values); /* 2 - confirm select-all option UNchecked */ await inputs.at(0).setChecked(false); diff --git a/client/src/components/Form/Elements/FormCheck.vue b/client/src/components/Form/Elements/FormCheck.vue index de8dad4f92c4..879b4411dd2a 100644 --- a/client/src/components/Form/Elements/FormCheck.vue +++ b/client/src/components/Form/Elements/FormCheck.vue @@ -1,9 +1,14 @@ + + diff --git a/client/src/components/Form/Elements/FormData/types.ts b/client/src/components/Form/Elements/FormData/types.ts new file mode 100644 index 000000000000..46929c8b876d --- /dev/null +++ b/client/src/components/Form/Elements/FormData/types.ts @@ -0,0 +1,10 @@ +export interface DataOption { + id: string; + hid: number; + is_dataset?: boolean; + keep: boolean; + map_over_type?: string; + name: string; + src: string; + tags: Array; +} diff --git a/client/src/components/Form/Elements/FormData/variants.ts b/client/src/components/Form/Elements/FormData/variants.ts new file mode 100644 index 000000000000..1b73712514fe --- /dev/null +++ b/client/src/components/Form/Elements/FormData/variants.ts @@ -0,0 +1,127 @@ +/** Data input variations interface */ +interface VariantInterface { + batch: string; + icon: string; + library?: boolean; + multiple: boolean; + src: string; + tooltip: string; +} + +/** Batch mode variations */ +export const BATCH = { DISABLED: "disabled", ENABLED: "enabled", LINKED: "linked" }; + +/** Data source variations */ +export const SOURCE = { DATASET: "hda", COLLECTION: "hdca", COLLECTION_ELEMENT: "dce", LIBRARY_DATASET: "ldda" }; + +/** List of available data input variations */ +export const VARIANTS: Record> = { + data: [ + { + src: "hda", + icon: "fa-file", + tooltip: "Single dataset", + library: true, + multiple: false, + batch: BATCH.DISABLED, + }, + { + src: "hda", + icon: "fa-copy", + tooltip: "Multiple datasets", + multiple: true, + batch: BATCH.LINKED, + }, + { + src: "hdca", + icon: "fa-folder", + tooltip: "Dataset collection", + multiple: false, + batch: BATCH.LINKED, + }, + ], + data_multiple: [ + { + src: "hda", + icon: "fa-copy", + tooltip: "Multiple datasets", + multiple: true, + batch: BATCH.DISABLED, + }, + { + src: "hdca", + icon: "fa-folder", + tooltip: "Dataset collection", + multiple: true, + batch: BATCH.DISABLED, + }, + ], + data_collection: [ + { + src: "hdca", + icon: "fa-folder", + tooltip: "Dataset collection", + multiple: false, + batch: BATCH.DISABLED, + }, + ], + workflow_data: [ + { + src: "hda", + icon: "fa-file", + tooltip: "Single dataset", + multiple: false, + batch: BATCH.DISABLED, + }, + ], + workflow_data_multiple: [ + { + src: "hda", + icon: "fa-copy", + tooltip: "Multiple datasets", + multiple: true, + batch: BATCH.DISABLED, + }, + ], + workflow_data_collection: [ + { + src: "hdca", + icon: "fa-folder", + tooltip: "Dataset collection", + multiple: false, + batch: BATCH.DISABLED, + }, + ], + module_data: [ + { + src: "hda", + icon: "fa-file", + tooltip: "Single dataset", + multiple: false, + batch: BATCH.DISABLED, + }, + { + src: "hda", + icon: "fa-copy", + tooltip: "Multiple datasets", + multiple: true, + batch: BATCH.ENABLED, + }, + ], + module_data_collection: [ + { + src: "hdca", + icon: "fa-folder", + tooltip: "Dataset collection", + multiple: false, + batch: BATCH.DISABLED, + }, + { + src: "hdca", + icon: "fa-folder", + tooltip: "Multiple collections", + multiple: true, + batch: BATCH.ENABLED, + }, + ], +}; diff --git a/client/src/components/Form/Elements/FormNumber.vue b/client/src/components/Form/Elements/FormNumber.vue index efdd9bfbd935..a616bd3d8706 100644 --- a/client/src/components/Form/Elements/FormNumber.vue +++ b/client/src/components/Form/Elements/FormNumber.vue @@ -8,16 +8,16 @@ - + diff --git a/client/src/components/Form/Elements/FormRadio.test.js b/client/src/components/Form/Elements/FormRadio.test.js index 1735456a4a9f..9328055ec30e 100644 --- a/client/src/components/Form/Elements/FormRadio.test.js +++ b/client/src/components/Form/Elements/FormRadio.test.js @@ -24,7 +24,7 @@ describe("FormRadio", () => { const n = 3; const options = []; for (let i = 0; i < n; i++) { - options.push([`label_${i}`, `value_${i}`]); + options.push({ label: `label_${i}`, value: `value_${i}` }); } await wrapper.setProps({ options }); const inputs = wrapper.findAll("[type='radio']"); diff --git a/client/src/components/Form/Elements/FormRadio.vue b/client/src/components/Form/Elements/FormRadio.vue index ec133dfe3f14..561870f3e44a 100644 --- a/client/src/components/Form/Elements/FormRadio.vue +++ b/client/src/components/Form/Elements/FormRadio.vue @@ -28,8 +28,8 @@ const hasOptions = computed(() => { diff --git a/client/src/components/Form/Elements/FormSelection.vue b/client/src/components/Form/Elements/FormSelection.vue index 306a3a15686c..42d5a762e0d6 100644 --- a/client/src/components/Form/Elements/FormSelection.vue +++ b/client/src/components/Form/Elements/FormSelection.vue @@ -1,9 +1,9 @@ diff --git a/client/src/components/Form/Elements/parameters.js b/client/src/components/Form/Elements/parameters.js index 39761634e9d3..01729e887c0a 100644 --- a/client/src/components/Form/Elements/parameters.js +++ b/client/src/components/Form/Elements/parameters.js @@ -1,29 +1,16 @@ /** This class creates input elements. New input parameter types should be added to the types dictionary. */ -import { getGalaxyInstance } from "app"; import Backbone from "backbone"; -import DataPicker from "mvc/ui/ui-data-picker"; -import Ui from "mvc/ui/ui-misc"; -import SelectContent from "mvc/ui/ui-select-content"; import SelectFtp from "mvc/ui/ui-select-ftp"; import SelectLibrary from "mvc/ui/ui-select-library"; -import Utils from "utils/utils"; // create form view export default Backbone.View.extend({ /** Available parameter types */ types: { - select: "_fieldSelect", - data_column: "_fieldSelect", - genomebuild: "_fieldSelect", - data: "_fieldData", - data_collection: "_fieldData", - group_tag: "_fieldSelect", library_data: "_fieldLibrary", ftpfile: "_fieldFtp", - rules: "_fieldRulesEdit", - data_dialog: "_fieldDialog", }, remove: function () { @@ -35,10 +22,6 @@ export default Backbone.View.extend({ create: function (input_def) { var fieldClass = this.types[input_def.type]; this.field = typeof this[fieldClass] === "function" ? this[fieldClass].call(this, input_def) : null; - if (!this.field) { - this.field = input_def.options ? this._fieldSelect(input_def) : this._fieldText(input_def); - console.debug("form-parameters::_addRow()", `Auto matched field type (${input_def.type}).`); - } if (input_def.value === undefined) { input_def.value = null; } @@ -47,118 +30,6 @@ export default Backbone.View.extend({ this.$el.append(this.field.$el); }, - /** Data input field */ - _fieldData: function (input_def) { - return new SelectContent.View({ - id: input_def.id, - extensions: input_def.extensions, - optional: input_def.optional, - multiple: input_def.multiple, - type: input_def.type, - flavor: input_def.flavor, - data: input_def.options, - tag: input_def.tag, - onchange: input_def.onchange, - }); - }, - - /** Select/Checkbox/Radio options field */ - _fieldSelect: function (input_def) { - // show text field e.g. in workflow editor - if (input_def.is_workflow) { - return this._fieldText(input_def); - } - // customize properties - if (input_def.type == "data_column") { - input_def.error_text = "Missing columns in referenced dataset."; - } - - // pick selection display - var classes = { - checkboxes: Ui.Checkbox, - radio: Ui.Radio, - radiobutton: Ui.RadioButton, - }; - var SelectClass = classes[input_def.display] || Ui.Select; - // use Select2 fields or regular select fields in workflow launch form? - // check select_type_workflow_threshold option - const Galaxy = getGalaxyInstance(); - var searchable = true; - if (input_def.flavor == "workflow") { - if (Galaxy.config.select_type_workflow_threshold == -1) { - searchable = false; - } else if (Galaxy.config.select_type_workflow_threshold == 0) { - searchable = true; - } else if (Galaxy.config.select_type_workflow_threshold < input_def.options.length) { - searchable = false; - } - } - return new Ui.TextSelect({ - id: input_def.id, - data: input_def.data, - options: input_def.options, - display: input_def.display, - error_text: input_def.error_text || "No options available", - readonly: input_def.readonly, - multiple: input_def.multiple, - optional: input_def.optional, - onchange: input_def.onchange, - individual: input_def.individual, - searchable: searchable, - textable: input_def.textable, - SelectClass: SelectClass, - }); - }, - - /** Text input field */ - _fieldText: function (input_def) { - // field replaces e.g. a select field - const inputClass = input_def.optional && input_def.type === "select" ? Ui.NullableText : Ui.Input; - if ( - ["SelectTagParameter", "ColumnListParameter"].includes(input_def.model_class) || - (input_def.options && input_def.data) - ) { - input_def.area = input_def.multiple; - if (Utils.isEmpty(input_def.value)) { - input_def.value = null; - } else { - if (Array.isArray(input_def.value)) { - var str_value = ""; - for (var i in input_def.value) { - str_value += String(input_def.value[i]); - if (!input_def.multiple) { - break; - } - str_value += "\n"; - } - input_def.value = str_value; - } - } - } - // create input element - return new inputClass({ - id: input_def.id, - type: input_def.type, - area: input_def.area, - readonly: input_def.readonly, - color: input_def.color, - style: input_def.style, - placeholder: input_def.placeholder, - datalist: input_def.datalist, - onchange: input_def.onchange, - value: input_def.value, - }); - }, - - /** Data dialog picker field */ - _fieldDialog: function (input_def) { - return new DataPicker({ - id: input_def.id, - multiple: input_def.multiple, - onchange: input_def.onchange, - }); - }, - /** Library dataset field */ _fieldLibrary: function (input_def) { return new SelectLibrary.View({ diff --git a/client/src/components/Form/Elements/parameters.test.js b/client/src/components/Form/Elements/parameters.test.js deleted file mode 100644 index dc700bad1363..000000000000 --- a/client/src/components/Form/Elements/parameters.test.js +++ /dev/null @@ -1,44 +0,0 @@ -import Ui from "mvc/ui/ui-misc"; - -import ParameterFactory from "./parameters"; - -jest.mock("app"); -jest.mock("mvc/ui/ui-misc", () => ({ - TextSelect: jest.fn(() => { - return { - value: jest.fn(), - }; - }), - Input: jest.fn(() => { - return { - value: jest.fn(), - }; - }), -})); - -describe("ParameterFactory", () => { - it("should create a TEXT parameter input when no type is specified", async () => { - const input = { - id: "type-less parameter", - type: "", - value: "initial_value", - }; - const parameter = new ParameterFactory(); - parameter.create(input); - expect(Ui.Input).toHaveBeenCalled(); - expect(Ui.TextSelect).not.toHaveBeenCalled(); - }); - - it("should create a SELECT parameter input when no type is specified and the input has options", async () => { - const input = { - id: "type-less parameter with options", - type: "", - value: "initial_value", - options: [("Option A", "a"), ("Option B", "b")], - }; - const parameter = new ParameterFactory(); - parameter.create(input); - expect(Ui.TextSelect).toHaveBeenCalled(); - expect(Ui.Input).not.toHaveBeenCalled(); - }); -}); diff --git a/client/src/components/Form/FormDisplay.vue b/client/src/components/Form/FormDisplay.vue index f6e22c76fb7c..6fe1e7128ac5 100644 --- a/client/src/components/Form/FormDisplay.vue +++ b/client/src/components/Form/FormDisplay.vue @@ -2,6 +2,7 @@ (), { id: "identifier", refreshOnChange: false, - backbonejs: false, disabled: false, collapsedEnableText: "Enable", collapsedDisableText: "Disable", @@ -205,9 +205,9 @@ const isOptional = computed(() => !isRequired.value && attrs.value["optional"] ! - + + + !isRequired.value && attrs.value["optional"] ! :options="attrs.options" :optional="attrs.optional" :multiple="attrs.multiple" /> + !isRequired.value && attrs.value["optional"] !
@@ -86,6 +85,10 @@ export default { type: Array, default: null, }, + loading: { + type: Boolean, + default: false, + }, prefix: { type: String, default: "", diff --git a/client/src/components/Tool/ToolForm.vue b/client/src/components/Tool/ToolForm.vue index 3140f8771a4e..bcca2a7c2302 100644 --- a/client/src/components/Tool/ToolForm.vue +++ b/client/src/components/Tool/ToolForm.vue @@ -43,6 +43,7 @@ :id="toolId" :inputs="formConfig.inputs" :errors="formConfig.errors" + :loading="loading" :validation-scroll-to="validationScrollTo" :warnings="formConfig.warnings" @onChange="onChange" @@ -155,6 +156,7 @@ export default { data() { return { disabled: false, + loading: false, showLoading: true, showForm: false, showEntryPoints: false, @@ -271,6 +273,7 @@ export default { requestTool(newVersion) { this.currentVersion = newVersion || this.currentVersion; this.disabled = true; + this.loading = true; console.debug("ToolForm - Requesting tool.", this.id); return getToolFormData(this.id, this.currentVersion, this.job_id, this.history_id) .then((data) => { @@ -290,6 +293,7 @@ export default { }) .finally(() => { this.disabled = false; + this.loading = false; this.showLoading = false; }); }, diff --git a/client/src/components/Upload/UploadSelect.vue b/client/src/components/Upload/UploadSelect.vue index 36d36878f326..847fc160f4d0 100644 --- a/client/src/components/Upload/UploadSelect.vue +++ b/client/src/components/Upload/UploadSelect.vue @@ -97,6 +97,8 @@ const currentValue = computed({ margin: 0px; padding: 0px; .multiselect__single { + text-overflow: ellipsis; + white-space: nowrap; width: 130px; } } diff --git a/client/src/components/Workflow/Editor/Forms/FormOutput.vue b/client/src/components/Workflow/Editor/Forms/FormOutput.vue index 8d5bbf18cc83..a0e41c37eb40 100644 --- a/client/src/components/Workflow/Editor/Forms/FormOutput.vue +++ b/client/src/components/Workflow/Editor/Forms/FormOutput.vue @@ -15,7 +15,6 @@ :attributes="{ options: datatypeExtensions }" title="Change datatype" type="select" - backbonejs help="This action will change the datatype of the output to the indicated datatype." @input="onDatatype" /> diff --git a/client/src/components/Workflow/Run/WorkflowRunFormSimple.vue b/client/src/components/Workflow/Run/WorkflowRunFormSimple.vue index 719f44068ec8..77621c6d74de 100644 --- a/client/src/components/Workflow/Run/WorkflowRunFormSimple.vue +++ b/client/src/components/Workflow/Run/WorkflowRunFormSimple.vue @@ -15,20 +15,20 @@ - Send results to a new history + + Send results to a new history + Attempt to re-use jobs with identical parameters? + title="This may skip executing jobs that you have already run."> + Attempt to re-use jobs with identical parameters? + Send outputs and intermediate to different object stores? + v-model="splitObjectStore"> + Send outputs and intermediate to different object stores? + { - if (!isWorkflowInput(step.step_type)) { - return; + if (isWorkflowInput(step.step_type)) { + const stepName = new String(step.step_index); + const stepLabel = step.step_label || new String(step.step_index + 1); + const help = step.annotation; + const longFormInput = step.inputs[0]; + const stepAsInput = Object.assign({}, longFormInput, { + name: stepName, + help: help, + label: stepLabel, + }); + // disable collection mapping... + stepAsInput.flavor = "module"; + inputs.push(stepAsInput); + this.inputTypes[stepName] = step.step_type; } - const stepName = new String(step.step_index); - const stepLabel = step.step_label || new String(step.step_index + 1); - const help = step.annotation; - const longFormInput = step.inputs[0]; - const stepAsInput = Object.assign({}, longFormInput, { name: stepName, help: help, label: stepLabel }); - // disable collection mapping... - stepAsInput.flavor = "module"; - inputs.push(stepAsInput); - this.inputTypes[stepName] = step.step_type; }); return inputs; }, diff --git a/client/src/mvc/ui/ui-data-picker.js b/client/src/mvc/ui/ui-data-picker.js deleted file mode 100644 index 5a5672fa363d..000000000000 --- a/client/src/mvc/ui/ui-data-picker.js +++ /dev/null @@ -1,49 +0,0 @@ -/** Creates a data dialog input field */ -import { getGalaxyInstance } from "app/index"; -import Backbone from "backbone"; -import $ from "jquery"; -import Buttons from "mvc/ui/ui-buttons"; - -export default Backbone.View.extend({ - constructor(options) { - const Galaxy = getGalaxyInstance(); - this.model = (options && options.model) || new Backbone.Model(options); - this.button_dialog = new Buttons.Button({ - icon: "fa-folder-open-o", - tooltip: "Browse Datasets", - cls: "btn btn-secondary float-left mr-2", - onclick: () => { - Galaxy.data.dialog( - (response) => { - this.model.set("value", response); - if (this.model.get("onchange")) { - this.model.get("onchange")(response); - } - }, - { - multiple: Boolean(this.model.get("multiple")), - } - ); - }, - }); - this.$info = $("").addClass("ui-input float-left"); - this.setElement($("
").addClass("d-flex").append(this.button_dialog.$el).append(this.$info)); - this.listenTo(this.model, "change", this.render, this); - this.render(); - }, - value: function (new_val) { - if (new_val !== undefined) { - this.model.set("value", new_val); - } - return this.model.get("value"); - }, - render: function () { - this.$el.attr("id", this.model.id); - let label = this.model.get("value"); - if (label && this.model.get("multiple")) { - label = label.join(", "); - } - this.$info.val(label || "Empty"); - return this; - }, -}); diff --git a/client/src/mvc/ui/ui-misc.js b/client/src/mvc/ui/ui-misc.js index 451c7f785973..64da9a93ceca 100644 --- a/client/src/mvc/ui/ui-misc.js +++ b/client/src/mvc/ui/ui-misc.js @@ -7,7 +7,6 @@ import Buttons from "mvc/ui/ui-buttons"; import Modal from "mvc/ui/ui-modal"; import Options from "mvc/ui/ui-options"; import Select from "mvc/ui/ui-select-default"; -import Switch from "mvc/ui/ui-switch"; import _ from "underscore"; /** Displays messages used e.g. in the tool form */ @@ -124,103 +123,6 @@ export var Input = Backbone.View.extend({ }, }); -export var NullableText = Backbone.View.extend({ - initialize: function (options) { - this.model = (options && options.model) || new Backbone.Model().set(options); - - // Add text field - this.text_input = new Input(options); - - // Add button that determines whether an optional value should be defined - this.optional_button = new Switch({ - id: `optional-switch-${this.model.id}`, - }); - - // Create element - this.setElement("
"); - this.$el.append("
Set value for this optional select field?
"); - this.$el.append(this.optional_button.$el); - this.$el.append(this.text_input.$el); - - // Determine true/false value of button based on initial value - this.optional_button.model.set("value", this.text_input.model.get("value") === null ? "false" : "true"); - this.toggleButton(); - this.listenTo(this.optional_button.model, "change", this.toggleButton, this); - }, - toggleButton: function () { - const setOptional = this.optional_button.model.get("value"); - if (setOptional == "true") { - // Enable text field, set value to `""` if the value is falsy and trigger change - this.text_input.model.set("disabled", false); - if (!this.text_input.model.get("value")) { - this.text_input.model.set("value", ""); - this.model.get("onchange") && this.model.get("onchange")(""); - } - } else { - // Set text field to disabled, set model value to null and trigger change - this.text_input.model.set("disabled", true); - this.text_input.model.set("value", null); - this.model.get("onchange") && this.model.get("onchange")(null); - } - }, - value: function (new_val) { - const setOptional = this.optional_button.model.get("value"); - if (setOptional == "true") { - new_val !== undefined && this.model.set("value", typeof new_val == "string" ? new_val : ""); - } - return this.text_input.model.get("value"); - }, -}); - -/** Creates an input element which switches between select and text field */ -export var TextSelect = Backbone.View.extend({ - initialize: function (options) { - this.select = new options.SelectClass.View(options); - this.model = this.select.model; - const textInputClass = options.optional ? NullableText : Input; - this.text = new textInputClass({ - onchange: this.model.get("onchange"), - }); - this.on("change", () => { - if (this.model.get("onchange")) { - this.model.get("onchange")(this.value()); - } - }); - this.setElement($("
").append(this.select.$el).append(this.text.$el)); - this.update(options); - }, - remove: function () { - this.select.remove(); - this.text.remove(); - Backbone.View.prototype.remove.call(this); - }, - wait: function () { - this.select.wait(); - }, - unwait: function () { - this.select.unwait(); - }, - value: function (new_val) { - var element = this.textmode ? this.text : this.select; - return element.value(new_val); - }, - update: function (input_def) { - var data = input_def.data; - if (!data) { - data = []; - _.each(input_def.options, (option) => { - data.push({ label: option[0], value: option[1] }); - }); - } - var v = this.value(); - this.textmode = input_def.textable && (!Array.isArray(data) || data.length === 0); - this.text.$el[this.textmode ? "show" : "hide"](); - this.select.$el[this.textmode ? "hide" : "show"](); - this.select.update({ data: data }); - this.value(v); - }, -}); - /* Make more Ui stuff directly available at this namespace (for backwards * compatibility). We should eliminate this practice, though, and just require * what we need where we need it, allowing for better package optimization. @@ -248,6 +150,4 @@ export default { Checkbox: Options.Checkbox, Radio: Options.Radio, Select: Select, - NullableText: NullableText, - TextSelect: TextSelect, }; diff --git a/client/src/mvc/ui/ui-select-content.js b/client/src/mvc/ui/ui-select-content.js deleted file mode 100644 index 3162bf6eaa56..000000000000 --- a/client/src/mvc/ui/ui-select-content.js +++ /dev/null @@ -1,626 +0,0 @@ -import { getGalaxyInstance } from "app"; -import Backbone from "backbone"; -import $ from "jquery"; -import Ui from "mvc/ui/ui-misc"; -import Select from "mvc/ui/ui-select-default"; -import _ from "underscore"; -import _l from "utils/localization"; -import Utils from "utils/utils"; - -/** Batch mode variations */ -const Batch = { DISABLED: "disabled", ENABLED: "enabled", LINKED: "linked" }; - -/** List of available content selectors options */ -const Configurations = { - data: [ - { - src: "hda", - icon: "fa-file-o", - tooltip: _l("Single dataset"), - library: true, - multiple: false, - batch: Batch.DISABLED, - }, - { - src: "hda", - icon: "fa-files-o", - tooltip: _l("Multiple datasets"), - multiple: true, - batch: Batch.LINKED, - }, - { - src: "hdca", - icon: "fa-folder-o", - tooltip: _l("Dataset collection"), - multiple: false, - batch: Batch.LINKED, - }, - ], - data_multiple: [ - { - src: "hda", - icon: "fa-files-o", - tooltip: _l("Multiple datasets"), - multiple: true, - batch: Batch.DISABLED, - }, - { - src: "hdca", - icon: "fa-folder-o", - tooltip: _l("Dataset collections"), - multiple: true, - batch: Batch.DISABLED, - }, - ], - data_collection: [ - { - src: "hdca", - icon: "fa-folder-o", - tooltip: _l("Dataset collection"), - multiple: false, - batch: Batch.DISABLED, - }, - ], - workflow_data: [ - { - src: "hda", - icon: "fa-file-o", - tooltip: _l("Single dataset"), - multiple: false, - batch: Batch.DISABLED, - }, - ], - workflow_data_multiple: [ - { - src: "hda", - icon: "fa-files-o", - tooltip: _l("Multiple datasets"), - multiple: true, - batch: Batch.DISABLED, - }, - ], - workflow_data_collection: [ - { - src: "hdca", - icon: "fa-folder-o", - tooltip: _l("Dataset collection"), - multiple: false, - batch: Batch.DISABLED, - }, - ], - module_data: [ - { - src: "hda", - icon: "fa-file-o", - tooltip: _l("Single dataset"), - multiple: false, - batch: Batch.DISABLED, - }, - { - src: "hda", - icon: "fa-files-o", - tooltip: _l("Multiple datasets"), - multiple: true, - batch: Batch.ENABLED, - }, - ], - module_data_collection: [ - { - src: "hdca", - icon: "fa-folder-o", - tooltip: _l("Dataset collection"), - multiple: false, - batch: Batch.DISABLED, - }, - { - src: "hdca", - icon: "fa-folder", - tooltip: _l("Multiple collections"), - multiple: true, - batch: Batch.ENABLED, - }, - ], -}; - -/** View for hda and dce content selector ui elements */ -const View = Backbone.View.extend({ - initialize: function (options) { - const self = this; - this.model = - (options && options.model) || - new Backbone.Model({ - src_labels: { hda: "dataset", hdca: "dataset collection" }, - pagelimit: 100, - statustimer: 1000, - }).set(options); - this.setElement($("
").addClass("ui-select-content")); - this.button_product = new Ui.RadioButton.View({ - value: "false", - data: [ - { - icon: "fa fa-chain", - value: "false", - tooltip: - "Linked inputs will be run in matched order with other datasets e.g. use this for matching forward and reverse reads.", - }, - { - icon: "fa fa-chain-broken", - value: "true", - tooltip: "Unlinked dataset inputs will be run against *all* other inputs.", - }, - ], - }); - this.$batch = { - linked: $(this._templateBatch()).clone(), - enabled: $(this._templateBatch()).clone().append(this.button_product.$el), - }; - - // add drag-drop event handlers - const element = this.$el.get(0); - element.addEventListener("dragenter", (e) => { - this.lastenter = e.target; - self.$el.addClass("ui-dragover"); - }); - element.addEventListener("dragover", (e) => { - e.preventDefault(); - }); - element.addEventListener("dragleave", (e) => { - this.lastenter === e.target && self.$el.removeClass("ui-dragover"); - }); - element.addEventListener("drop", (e) => { - e.preventDefault(); - try { - const drop_data = JSON.parse(e.dataTransfer.getData("text"))[0]; - this._handleDropValues(drop_data); - } catch (e) { - this._handleDropStatus("danger"); - } - }); - - // track current cache elements - this.cache = {}; - - // add listeners - this.listenTo(this.model, "change:data", this._changeData, this); - this.listenTo(this.model, "change:wait", this._changeWait, this); - this.listenTo(this.model, "change:current", this._changeCurrent, this); - this.listenTo(this.model, "change:value", this._changeValue, this); - this.listenTo( - this.model, - "change:type change:optional change:multiple change:extensions", - this._changeType, - this - ); - this.render(); - - // add change event - this.on("change", () => { - options.onchange && options.onchange(self.value()); - }); - }, - - render: function () { - this._changeType(); - this._changeValue(); - this._changeWait(); - }, - - /** Indicate that select fields are being updated */ - wait: function () { - this.model.set("wait", true); - }, - - /** Indicate that the options update has been completed */ - unwait: function () { - this.model.set("wait", false); - }, - - /** Update data representing selectable options */ - update: function (input_def) { - this.model.set("data", input_def.options); - }, - - /** Return the currently selected dataset values */ - value: function (new_value) { - if (new_value) { - this._patchValue(new_value); - const result = this._batch(this._pickValue(new_value)); - this.model.set("value", result); - } - const current = this.model.get("current"); - if (this.config[current]) { - let id_list = this.fields[current].value(); - if (id_list !== null) { - id_list = Array.isArray(id_list) ? id_list : [id_list]; - if (id_list.length > 0) { - const result = this._batch({ values: [] }); - for (const i in id_list) { - const details = this.cache[`${id_list[i]}_${this.config[current].src}`]; - if (details) { - const unpatchedValue = this._unpatchValue(details); - result.values.push(unpatchedValue); - } else { - console.debug( - "ui-select-content::value()", - `Requested details not found for '${id_list[i]}'.` - ); - return null; - } - } - result.values.sort((a, b) => a.hid - b.hid); - return this._pickValue(result); - } - } - } else { - console.debug("ui-select-content::value()", `Invalid value/source '${new_value}'.`); - } - return null; - }, - - /** Change of current select field */ - _changeCurrent: function () { - _.each(this.fields, (field, i) => { - const cnf = this.config[i]; - if (this.model.get("current") == i) { - field.$el.show(); - _.each(this.$batch, ($batchfield, batchmode) => { - if (cnf.batch == batchmode) { - $batchfield.show(); - } else { - $batchfield.hide(); - } - }); - if (cnf.src == "hda") { - this.button_dialog.show(); - this.upload_dialog.show(); - } else { - this.button_dialog.hide(); - this.upload_dialog.hide(); - } - this.button_type.value(i); - } else { - field.$el.hide(); - } - }); - if (this.fields.length > 1) { - this.button_type.show(); - } else { - this.button_type.hide(); - } - }, - - /** Change of type */ - _changeType: function () { - const self = this; - const galaxy = getGalaxyInstance(); - - // identify selector type identifier i.e. [ flavor ]_[ type ]_[ multiple ] - const config_id = - (this.model.get("flavor") ? `${this.model.get("flavor")}_` : "") + - String(this.model.get("type")) + - (this.model.get("multiple") ? "_multiple" : ""); - if (Configurations[config_id]) { - this.config = Configurations[config_id]; - } else { - this.config = Configurations["data"]; - console.debug("ui-select-content::_changeType()", `Invalid configuration/type id '${config_id}'.`); - } - - // prepare extension component of error message - const data = self.model.get("data"); - const formats = this.model.get("extensions"); - const extensions = Array.isArray(formats) ? Utils.textify(formats) : ""; - const src_labels = this.model.get("src_labels"); - - // build radio button for data selectors - this.fields = []; - this.button_data = []; - _.each(this.config, (c, i) => { - self.button_data.push({ - value: i, - icon: c.icon, - tooltip: c.tooltip, - }); - self.fields.push( - new Select.View({ - optional: self.model.get("optional"), - multiple: c.multiple, - searchable: - !c.multiple || (data && data[c.src] && data[c.src].length > self.model.get("pagelimit")), - individual: true, - error_text: `No ${extensions ? `${extensions} ` : ""}${src_labels[c.src] || "content"} available.`, - onchange: function () { - self.trigger("change"); - }, - }) - ); - }); - this.button_type = new Ui.RadioButton.View({ - value: this.model.get("current"), - data: this.button_data, - cls: "pr-lg-2", - onchange: function (value) { - self.model.set("current", value); - self.trigger("change"); - }, - }); - - // build data dialog button - this.button_dialog = new Ui.Button({ - icon: "fa-folder-open-o", - tooltip: "Browse Datasets", - onclick: () => { - const current = this.model.get("current"); - const cnf = this.config[current]; - galaxy.data.dialog( - (response) => { - this._handleDropValues(response, false); - }, - { - multiple: cnf.multiple, - library: !!cnf.library, - format: null, - allowUpload: true, - } - ); - }, - }); - - // build data dialog button - this.upload_dialog = new Ui.Button({ - icon: "fa-upload", - tooltip: "Upload Dataset", - onclick: () => { - const current = this.model.get("current"); - const cnf = this.config[current]; - // model doesn't have format yet unfortunately, need to get through to here. - galaxy.data.dialog( - (response) => { - this._handleDropValues(response, false); - }, - { - multiple: cnf.multiple, - formats: formats, - new: true, - } - ); - }, - }); - - // append views - const $fields = $("
").addClass("overflow-auto w-100 py-2 py-lg-0 pr-lg-2 "); - this.$el - .empty() - .addClass("d-flex flex-row flex-wrap flex-lg-nowrap") - .append($("
").append(this.button_type.$el)) - .append($fields); - if (galaxy.config.upload_from_form_button == "always-on") { - this.$el.append($("
").append(this.upload_dialog.$el)); - } - this.$el.append($("
").append(this.button_dialog.$el)); - _.each(this.fields, (field) => { - $fields.append(field.$el); - }); - _.each(this.$batch, ($batchfield, batchmode) => { - $fields.append($batchfield); - }); - this.model.set("current", 0); - this._changeCurrent(); - this._changeData(); - }, - - /** Change of wait flag */ - _changeWait: function () { - const self = this; - _.each(this.fields, (field) => { - field[self.model.get("wait") ? "wait" : "unwait"](); - }); - }, - - /** Change of available options */ - _changeData: function () { - const options = this.model.get("data"); - const self = this; - const select_options = { hda: [], hdca: [] }; - _.each(options, (items, src) => { - _.each(items, (item) => { - self._patchValue(item, src); - const current_src = item.src || src; - const addOption = !this.model.attributes.tag || item.tags.includes(this.model.attributes.tag); - if (addOption) { - select_options[current_src].push({ - hid: item.hid || Infinity, // if we got no hid we have a "Selected" item - keep: item.keep, - label: `${item.hid || "Selected"}: ${item.name}`, - value: item.id, - origin: item.origin, - tags: item.tags, - }); - } - self.cache[`${item.id}_${current_src}`] = item; - }); - }); - _.each(this.config, (c, i) => { - select_options[c.src] && self.fields[i].add(select_options[c.src], (a, b) => b.hid - a.hid); - }); - }, - - /** Change of incoming value */ - _changeValue: function () { - const new_value = this.model.get("value"); - if (new_value && new_value.values && new_value.values.length > 0) { - // sniff first suitable field type from config list - let src = new_value.values[0].src; - if (src === "dce") { - src = - this.cache[`dce${new_value.values[0].id}_hda`]?.src || - this.cache[`dce${new_value.values[0].id}_hdca`]?.src; - } - this._patchValue(new_value, src); - // create list with content ids - const list = []; - _.each(new_value.values, (value) => { - list.push(value.id); - }); - const multiple = new_value.values.length > 1; - for (let i = 0; i < this.config.length; i++) { - const field = this.fields[i]; - const c = this.config[i]; - if (c.src == src && [multiple, true].indexOf(c.multiple) !== -1) { - this.model.set("current", i); - field.value(list); - break; - } - } - } else { - _.each(this.fields, (field) => { - field.value(null); - }); - } - }, - - /** Source helper matches history_content_types to source types */ - _getSource: function (v) { - return v.history_content_type == "dataset_collection" ? "hdca" : "hda"; - }, - - /** Only utilize id, source and map_over_type when specifiying input value **/ - _pickValue: function (v) { - if (v && v.values) { - v.values = v.values.map((entry) => ({ - id: entry.id, - src: entry.src, - map_over_type: entry.map_over_type, - })); - } - return v; - }, - - /** Library datasets are displayed and selected together with history datasets, - Dataset collection elements are displayed together with history dataset collections **/ - _patchValue: function (v, src) { - const patchTo = { ldda: "hda", dce: src }; - if (v.values) { - _.each(v.values, (v) => { - this._patchValue(v, src); - }); - } else if (patchTo[v.src]) { - v.origin = v.src; - v.src = patchTo[v.src]; - v.id = `${v.origin}${v.id}`; - } - }, - - /** Restores original value e.g. after patching library datasets **/ - _unpatchValue: function (v) { - if (v.origin) { - const d = Object.assign({}, v); - d.id = d.id.substr(d.origin.length); - d.src = d.origin; - return d; - } - return v; - }, - - /** Add values from drag/drop */ - _handleDropValues: function (drop_data, drop_partial = true) { - const self = this; - const data = this.model.get("data"); - const current = this.model.get("current"); - const config = this.config[current]; - const field = this.fields[current]; - if (data) { - const values = Array.isArray(drop_data) ? drop_data : [drop_data]; - if (values.length > 0) { - let data_changed = false; - _.each(values, (v) => { - // element_id deals with override in old backbone code, - // can remove when old history is deprecated - v.id = v.element_id || v.id; - self._patchValue(v); - const new_id = v.id; - const new_src = (v.src = this._getSource(v)); - const new_value = { id: new_id, src: new_src }; - if (!_.findWhere(data[new_src], new_value)) { - data_changed = true; - data[new_src].push({ - id: new_id, - src: new_src, - hid: v.hid || "Selected", - name: v.name ? v.name : new_id, - origin: v.origin, - keep: true, - tags: [], - }); - } - }); - if (data_changed) { - this._changeData(); - } - const first_id = values[0].id; - const first_src = values[0].src; - if (config.src == first_src && drop_partial) { - let current_value = field.value(); - if (current_value && config.multiple) { - _.each(values, (v) => { - if (current_value.indexOf(v.id) == -1) { - current_value.push(v.id); - } - }); - } else { - current_value = first_id; - } - field.value(current_value); - } else { - this.model.set("value", { values: values }); - this.model.trigger("change:value"); - } - this.trigger("change"); - } - } - this._handleDropStatus("success"); - }, - - /** Highlight drag result */ - _handleDropStatus: function (status) { - const self = this; - this.$el.removeClass("ui-dragover").addClass(`ui-dragover-${status}`); - setTimeout(() => { - self.$el.removeClass(`ui-dragover-${status}`); - }, this.model.get("statustimer")); - }, - - /** Assists in identifying the batch mode */ - _batch: function (result) { - result["batch"] = false; - const current = this.model.get("current"); - const config = this.config[current]; - if (config.src == "dce" || config.src == "hdca") { - const element = this.cache[`${this.fields[current].value()}_${config.src}`]; - if (element && element.map_over_type) { - result["batch"] = true; - } - } - if (config.batch == Batch.LINKED || config.batch == Batch.ENABLED) { - result["batch"] = true; - if (config.batch == Batch.ENABLED && this.button_product.value() === "true") { - result["product"] = true; - } - } - return result; - }, - - /** Template for batch mode execution options */ - _templateBatch: function () { - return `
- - - This is a batch mode input field. Separate jobs will be triggered for each dataset selection. - -
`; - }, -}); - -export default { - View: View, -}; diff --git a/client/src/style/scss/multiselect.scss b/client/src/style/scss/multiselect.scss index f24dcdce59bd..cafc6647eb9f 100644 --- a/client/src/style/scss/multiselect.scss +++ b/client/src/style/scss/multiselect.scss @@ -21,6 +21,8 @@ } .multiselect__tag { min-height: 0; + white-space: normal; + word-break: break-all; } .multiselect__tags { background: transparent; @@ -31,8 +33,8 @@ color: $text-color; font-size: $font-size-base; overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + white-space: normal; + word-break: break-all; } } } diff --git a/client/src/style/scss/ui.scss b/client/src/style/scss/ui.scss index 4c4ac1be0c8e..c45fd5b9876c 100644 --- a/client/src/style/scss/ui.scss +++ b/client/src/style/scss/ui.scss @@ -353,23 +353,25 @@ $ui-margin-horizontal-large: $margin-v * 2; } } -.ui-dragover { +.ui-dragover-warning { + @extend .rounded; @extend .p-1; - border-radius: $border-radius-base; - border: 2px solid $state-warning-bg; background: lighten($state-warning-bg, 10%); + border: 2px solid $state-warning-bg; } .ui-dragover-danger { - @extend .ui-dragover; - border: 2px solid $state-danger-bg; + @extend .rounded; + @extend .p-1; background: lighten($state-danger-bg, 10%); + border: 2px solid $state-danger-bg; } .ui-dragover-success { - @extend .ui-dragover; - border: 2px solid $state-success-bg; + @extend .rounded; + @extend .p-1; background: lighten($state-success-bg, 10%); + border: 2px solid $state-success-bg; } /* Used for tree in tool recommendations */ diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml index 874ee317fbaf..52cbbe2886bc 100644 --- a/client/src/utils/navigation/navigation.yml +++ b/client/src/utils/navigation/navigation.yml @@ -555,13 +555,10 @@ 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' - parameter_data_input_single: - type: xpath - selector: //div[@id='form-element-${parameter}']//i[contains(@class, 'fa-file-o')]/parent::label - parameter_batch_dataset_collection: - type: xpath - selector: //div[@id='form-element-${parameter}']//i[contains(@class, 'fa-folder-o')]/parent::label - data_option_value: option[value="${item_id}"] + parameter_data_input_single: 'div.ui-form-element[id="form-element-${parameter}"] button[title="Single dataset"]' + parameter_data_input_collection: 'div.ui-form-element[id="form-element-${parameter}"] button[title="Dataset collection"]' + parameter_data_label: 'div.ui-form-element[id="form-element-${parameter}"] [data-description="form data label"]' + parameter_data_select: 'div.ui-form-element[id="form-element-${parameter}"] .multiselect' repeat_insert: '[data-description="repeat insert"]' repeat_move_up: '#${parameter}_up' @@ -648,7 +645,7 @@ workflow_run: selectors: warning: ".ui-form-composite .alert-warning" input_div: "[step-label='${label}']" - input_data_div: "[step-label='${label}'] .select2-container" + input_data_div: "[step-label='${label}'] .multiselect" # TODO: put step labels in the DOM ideally subworkflow_step_icon: ".portlet-title-icon.fa-sitemap" run_workflow: "#run-workflow" diff --git a/client/tests/jest/helpers.js b/client/tests/jest/helpers.js index 927c2cb71b21..2396578fe7e5 100644 --- a/client/tests/jest/helpers.js +++ b/client/tests/jest/helpers.js @@ -15,6 +15,12 @@ import { PiniaVuePlugin } from "pinia"; const defaultComparator = (a, b) => a == b; +export function dispatchEvent(wrapper, type, props = {}) { + const event = new Event(type, { bubbles: true }); + Object.assign(event, props); + wrapper.element.dispatchEvent(event); +} + export function findViaNavigation(wrapper, component) { return wrapper.find(component.selector); } diff --git a/lib/galaxy/selenium/navigates_galaxy.py b/lib/galaxy/selenium/navigates_galaxy.py index 30d16f4f0525..71764955fa4d 100644 --- a/lib/galaxy/selenium/navigates_galaxy.py +++ b/lib/galaxy/selenium/navigates_galaxy.py @@ -1484,12 +1484,13 @@ def workflow_run_with_name(self, name: str): self.workflow_index_open() self.workflow_index_search_for(name) self.workflow_click_option(".workflow-run") + self.sleep_for(self.wait_types.UX_RENDER) def workflow_run_specify_inputs(self, inputs: Dict[str, Any]): workflow_run = self.components.workflow_run for label, value in inputs.items(): input_div_element = workflow_run.input_data_div(label=label).wait_for_visible() - self.select2_set_value(input_div_element, "%d: " % value["hid"]) + self.select_set_value(input_div_element, "%d: " % value["hid"]) def workflow_run_submit(self): self.components.workflow_run.run_workflow.wait_for_and_click() @@ -1587,8 +1588,10 @@ def tool_set_value(self, expanded_parameter_id, value, expected_type=None): div_element = self.tool_parameter_div(expanded_parameter_id) assert div_element if expected_type in ["select", "data", "data_collection"]: - div_selector = f"div.ui-form-element[id$='form-element-{expanded_parameter_id}']" - self.select2_set_value(div_selector, value) + select_field = self.components.tool_form.parameter_data_select( + parameter=expanded_parameter_id + ).wait_for_visible() + self.select_set_value(select_field, value) else: input_element = div_element.find_element(By.CSS_SELECTOR, "input") # Clear default value diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py index 6de845c6fd32..6e16f7fc597c 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -2137,6 +2137,9 @@ def from_json(self, value, trans, other_values=None): elif isinstance(value, (HistoryDatasetAssociation, LibraryDatasetDatasetAssociation)): rval.append(value) elif isinstance(value, dict) and "src" in value and "id" in value: + if value["src"] == "ldda": + decoded_id = trans.security.decode_id(value["id"]) + rval.append(trans.sa_session.query(LibraryDatasetDatasetAssociation).get(decoded_id)) if value["src"] == "hda": decoded_id = trans.security.decode_id(value["id"]) rval.append(trans.sa_session.query(HistoryDatasetAssociation).get(decoded_id)) @@ -2272,7 +2275,7 @@ def to_dict(self, trans, other_values=None): # For consistency, should these just always be in the dict? d["min"] = self.min d["max"] = self.max - d["options"] = {"hda": [], "hdca": []} + d["options"] = {"dce": [], "ldda": [], "hda": [], "hdca": []} d["tag"] = self.tag # return dictionary without options if context is unavailable @@ -2300,26 +2303,27 @@ def append(list, hda, name, src, keep=False, subcollection_type=None): return list.append(value) def append_dce(dce): - if dce.hda: - # well this isn't good, but what's the alternative ? - # we should be precise about what we're (re-)running here. - key = "hda" - else: - key = "hdca" - d["options"][key].append( + d["options"]["dce"].append( { "id": trans.security.encode_id(dce.id), "name": dce.element_identifier, + "is_dataset": dce.hda is not None, "src": "dce", "tags": [], "keep": True, } ) - # append DCE - if isinstance(other_values.get(self.name), DatasetCollectionElement): - dce = other_values[self.name] - append_dce(dce) + def append_ldda(ldda): + d["options"]["ldda"].append( + { + "id": trans.security.encode_id(ldda.id), + "name": ldda.name, + "src": "ldda", + "tags": [], + "keep": True, + } + ) # add datasets hda_list = util.listify(other_values.get(self.name)) @@ -2342,6 +2346,8 @@ def append_dce(dce): append(d["options"]["hda"], hda, f"({hda_state}) {hda.name}", "hda", True) elif isinstance(hda, DatasetCollectionElement): append_dce(hda) + elif isinstance(hda, LibraryDatasetDatasetAssociation): + append_ldda(hda) # add dataset collections dataset_collection_matcher = dataset_matcher_factory.dataset_collection_matcher(dataset_matcher) @@ -2503,7 +2509,7 @@ def to_dict(self, trans, other_values=None): # append DCE if isinstance(other_values.get(self.name), DatasetCollectionElement): dce = other_values[self.name] - d["options"]["hdca"].append( + d["options"]["dce"].append( { "id": trans.security.encode_id(dce.id), "hid": -1, @@ -2538,10 +2544,10 @@ def to_dict(self, trans, other_values=None): { "id": trans.security.encode_id(hdca.id), "hid": hdca.hid, + "map_over_type": subcollection_type, "name": name, "src": "hdca", "tags": [t.user_tname if not t.value else f"{t.user_tname}:{t.value}" for t in hdca.tags], - "map_over_type": subcollection_type, } ) diff --git a/lib/galaxy_test/selenium/test_tool_form.py b/lib/galaxy_test/selenium/test_tool_form.py index 2bc8f59b2ac2..9dba37e40697 100644 --- a/lib/galaxy_test/selenium/test_tool_form.py +++ b/lib/galaxy_test/selenium/test_tool_form.py @@ -195,7 +195,7 @@ def test_rerun_deleted_dataset(self): ) assert error_col.text == "parameter 'col': an invalid option ('3') was selected (valid options: 1)" # validate errors when inputs are missing - self.components.tool_form.parameter_batch_dataset_collection(parameter="input1").wait_for_and_click() + self.components.tool_form.parameter_data_input_collection(parameter="input1").wait_for_and_click() self.sleep_for(self.wait_types.UX_TRANSITION) error_input1 = self.components.tool_form.parameter_error(parameter="input1").wait_for_visible() error_col = self.components.tool_form.parameter_error(parameter="col").wait_for_visible() @@ -223,15 +223,16 @@ def test_rerun_dataset_collection_element(self): history_id = self.current_history_id() # upload a nested collection - collection_id = self.dataset_collection_populator.create_list_of_list_in_history( + self.dataset_collection_populator.create_list_of_list_in_history( history_id, collection_type="list:list", wait=True, ).json()["id"] self.tool_open("identifier_multiple") - self.components.tool_form.parameter_batch_dataset_collection(parameter="input1").wait_for_and_click() + self.components.tool_form.parameter_data_input_collection(parameter="input1").wait_for_and_click() self.sleep_for(self.wait_types.UX_RENDER) - self.components.tool_form.data_option_value(item_id=collection_id).wait_for_and_click() + select_field = self.components.tool_form.parameter_data_select(parameter="input1") + self.select_set_value(select_field, "list:list") self.sleep_for(self.wait_types.UX_RENDER) self.tool_form_execute() self.history_panel_wait_for_hid_ok(7) @@ -241,7 +242,8 @@ def test_rerun_dataset_collection_element(self): self.sleep_for(self.wait_types.UX_RENDER) self.hda_click_primary_action_button(1, "rerun") self.sleep_for(self.wait_types.UX_RENDER) - assert self.driver.find_element(By.CSS_SELECTOR, "option:checked").text == "Selected: test0" + select_field = self.components.tool_form.parameter_data_select(parameter="input1") + self.select_set_value(select_field, "test0 (as dataset collection)") self.tool_form_execute() self.components.history_panel.collection_view.back_to_history.wait_for_and_click() self.history_panel_wait_for_hid_ok(9) diff --git a/lib/galaxy_test/selenium/test_workflow_editor.py b/lib/galaxy_test/selenium/test_workflow_editor.py index 7156a33962fb..9d3edcbc79c4 100644 --- a/lib/galaxy_test/selenium/test_workflow_editor.py +++ b/lib/galaxy_test/selenium/test_workflow_editor.py @@ -1076,7 +1076,7 @@ def test_map_over_output_indicator(self): self.workflow_editor_destroy_connection("filter#how|filter_source") self.assert_node_output_is("filter#output_filtered", "list") - def assert_node_output_is(self, label: str, output_type: str, map_over_type: Optional[str] = None): + def assert_node_output_is(self, label: str, output_type: str, subcollection_type: Optional[str] = None): editor = self.components.workflow_editor node_label, output_name = label.split("#") node = editor.node._(label=node_label) @@ -1085,18 +1085,18 @@ def assert_node_output_is(self, label: str, output_type: str, map_over_type: Opt self.hover_over(output_element) element = self.components._.tooltip_inner.wait_for_present() assert f"output is {output_type}" in element.text, element.text - if map_over_type is None: + if subcollection_type is None: assert "mapped-over" not in element.text else: fragment = " and mapped-over to produce a " - if map_over_type == "list:paired": + if subcollection_type == "list:paired": fragment += "list of pairs dataset collection" - elif map_over_type == "list:list": + elif subcollection_type == "list:list": fragment += "list of lists dataset collection" - elif map_over_type.count(":") > 1: - fragment += f"dataset collection with {map_over_type.count(':') + 1} levels of nesting" + elif subcollection_type.count(":") > 1: + fragment += f"dataset collection with {subcollection_type.count(':') + 1} levels of nesting" else: - fragment += f"{map_over_type}" + fragment += f"{subcollection_type}" assert fragment in element.text self.click_center()