diff --git a/client/src/components/Form/Elements/FormOptionalText.test.js b/client/src/components/Form/Elements/FormOptionalText.test.js
new file mode 100644
index 00000000000..ab859e305bb
--- /dev/null
+++ b/client/src/components/Form/Elements/FormOptionalText.test.js
@@ -0,0 +1,35 @@
+import { mount } from "@vue/test-utils";
+import { getLocalVue } from "tests/jest/helpers";
+import FormOptionalText from "./FormOptionalText";
+
+const localVue = getLocalVue();
+
+describe("FormOptionalText", () => {
+ const mountFormOptionalText = async (props) =>
+ await mount(FormOptionalText, {
+ propsData: props,
+ localVue,
+ });
+
+ it("should display existing values", async () => {
+ const v = "somevalue";
+ const wrapper = await mountFormOptionalText({ value: v });
+ const el = wrapper.find("input");
+ expect(el.element.checked).toEqual(true);
+ const elText = wrapper.find("input[type='text']");
+ expect(elText.element.value).toEqual(v);
+ await el.setChecked(false);
+ expect(wrapper.emitted().input[0][0]).toEqual(null);
+ await el.setChecked(true);
+ expect(wrapper.emitted().input[1][0]).toEqual("");
+ });
+
+ it("should initialize with null if value does not exist", async () => {
+ const v = null;
+ const wrapper = await mountFormOptionalText({ value: v });
+ const el = wrapper.find("input");
+ expect(el.element.checked).toEqual(false);
+ await wrapper.setProps({ value: "" });
+ expect(el.element.checked).toEqual(true);
+ });
+});
diff --git a/client/src/components/Form/Elements/FormOptionalText.vue b/client/src/components/Form/Elements/FormOptionalText.vue
new file mode 100644
index 00000000000..d399d48af58
--- /dev/null
+++ b/client/src/components/Form/Elements/FormOptionalText.vue
@@ -0,0 +1,91 @@
+
+
+
+ Set value for this optional select field?
+
+
+
+
+
+
diff --git a/client/src/components/Form/Elements/FormText.test.js b/client/src/components/Form/Elements/FormText.test.js
new file mode 100644
index 00000000000..0a4c2953b06
--- /dev/null
+++ b/client/src/components/Form/Elements/FormText.test.js
@@ -0,0 +1,78 @@
+import { mount } from "@vue/test-utils";
+import { getLocalVue } from "tests/jest/helpers";
+import FormText from "./FormText";
+
+const localVue = getLocalVue();
+
+describe("FormText", () => {
+ const mountFormText = async (props) =>
+ await mount(FormText, {
+ propsData: props,
+ localVue,
+ });
+
+ it("should render the appropriate input type", async () => {
+ const wrapper = await mountFormText({});
+ const el = wrapper.find("input[type='text']");
+ expect(el.exists()).toBe(true);
+ await wrapper.setProps({ type: "password" });
+ const elPassword = wrapper.find("input[type='password']");
+ expect(elPassword.exists()).toBe(true);
+ await wrapper.setProps({ type: "anyothertype" });
+ const elOtherType = wrapper.find("input[type='text']");
+ expect(elOtherType.exists()).toBe(true);
+ });
+
+ it("should render the appropriate component", async () => {
+ const wrapper = await mountFormText({ area: true });
+ const el = wrapper.find("textarea");
+ expect(el.exists()).toBe(true);
+ await wrapper.setProps({ area: false, multiple: true });
+ const elMultiple = wrapper.find("textarea");
+ expect(elMultiple.exists()).toBe(true);
+ });
+
+ it("should be able to render a datalist", async () => {
+ const wrapper = await mountFormText({ id: "text-input", datalist: ["one", "two", "three"] });
+ let el = wrapper.find("datalist");
+ expect(el.exists()).toBe(true);
+ el = wrapper.find("option");
+ expect(el.exists()).toBe(true);
+ el = wrapper.find("[list='text-input-datalist']");
+ expect(el.exists()).toBe(true);
+ });
+
+ it("should be able to render border and text color from props", async () => {
+ const wrapper = await mountFormText({});
+ const el = wrapper.find("input");
+ expect(el.element.style).toMatchObject({});
+ await wrapper.setProps({ color: "green" });
+ expect(el.element.style).toMatchObject({ color: "green", "border-color": "green" });
+ await wrapper.setProps({ cls: "my-custom-class" });
+ expect(el.element.classList).toContain("my-custom-class");
+ });
+
+ it("should be able to accept a default value", async () => {
+ const v = "something";
+ const wrapper = await mountFormText({ value: v });
+ const el = wrapper.find("input");
+ expect(el.props("value")).toEqual(v);
+ });
+
+ it("should be able to accept an array as value", async () => {
+ const v = ["field_1", "field_2", "field_3"];
+ const wrapper = await mountFormText({ value: v });
+ const el = wrapper.find("input");
+ expect(el.props("value")).toEqual("field_1");
+ await wrapper.setProps({ multiple: true });
+ const elMultiple = wrapper.find("textarea");
+ expect(elMultiple.props("value")).toEqual("field_1\nfield_2\nfield_3\n");
+ });
+
+ it("should be able to accept an empty array as value", async () => {
+ const v = [];
+ const wrapper = await mountFormText({ value: v });
+ const el = wrapper.find("input");
+ expect(el.props("value")).toEqual("");
+ });
+});
diff --git a/client/src/components/Form/Elements/FormText.vue b/client/src/components/Form/Elements/FormText.vue
new file mode 100644
index 00000000000..8a07155ae4f
--- /dev/null
+++ b/client/src/components/Form/Elements/FormText.vue
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/Form/FormElement.test.js b/client/src/components/Form/FormElement.test.js
index ef20e91452d..79a7770442f 100644
--- a/client/src/components/Form/FormElement.test.js
+++ b/client/src/components/Form/FormElement.test.js
@@ -2,7 +2,7 @@ import { mount } from "@vue/test-utils";
import { getLocalVue } from "tests/jest/helpers";
import FormElement from "./FormElement";
import FormHidden from "./Elements/FormHidden";
-import FormInput from "./Elements/FormInput";
+import FormText from "./Elements/FormText";
const localVue = getLocalVue();
@@ -70,12 +70,17 @@ describe("FormElement", () => {
it("check type matching", async () => {
await wrapper.setProps({ type: "text" });
- expect(wrapper.findComponent(FormInput).exists()).toBe(true);
+ expect(wrapper.findComponent(FormText).exists()).toBe(true);
expect(wrapper.findComponent(FormHidden).exists()).toBe(false);
await wrapper.setProps({ attributes: { titleonly: true } });
expect(wrapper.findComponent(FormHidden).exists()).toBe(true);
- expect(wrapper.findComponent(FormInput).exists()).toBe(false);
+ expect(wrapper.findComponent(FormText).exists()).toBe(false);
+ });
+
+ it("displays as the correct type if is_workflow is true", async () => {
+ await wrapper.setProps({ type: "data_column", attributes: { is_workflow: true } });
+ expect(wrapper.findComponent(FormText).exists()).toBe(true);
});
it("marks required values", async () => {
diff --git a/client/src/components/Form/FormElement.vue b/client/src/components/Form/FormElement.vue
index 3906594e94a..9df07595047 100644
--- a/client/src/components/Form/FormElement.vue
+++ b/client/src/components/Form/FormElement.vue
@@ -7,6 +7,8 @@ import FormSelection from "./Elements/FormSelection.vue";
import FormColor from "./Elements/FormColor.vue";
import FormDirectory from "./Elements/FormDirectory.vue";
import FormNumber from "./Elements/FormNumber.vue";
+import FormText from "./Elements/FormText.vue";
+import FormOptionalText from "./Elements/FormOptionalText.vue";
import FormRulesEdit from "./Elements/FormRulesEdit.vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { ref, computed, useAttrs } from "vue";
@@ -214,7 +216,6 @@ const isOptional = computed(() => !isRequired.value && attrs.value["optional"] !
- optional
-
@@ -226,6 +227,33 @@ const isOptional = computed(() => !isRequired.value && attrs.value["optional"] !
:min="attrs.min"
:type="props.type ?? 'float'"
:workflow-building-mode="workflowBuildingMode" />
+
+
!isRequired.value && attrs.value["optional"] !
:multiple="attrs.multiple" />
-
+
{
diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml
index 1167759fe3a..869da96d6ae 100644
--- a/client/src/utils/navigation/navigation.yml
+++ b/client/src/utils/navigation/navigation.yml
@@ -486,7 +486,7 @@ tool_form:
options: '.tool-dropdown'
execute: 'button#execute'
parameter_div: 'div.ui-form-element[id="form-element-${parameter}"]'
- parameter_checkbox: 'div.ui-form-element[id="form-element-${parameter}"] .ui-switch div'
+ 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'
reference: '.formatted-reference'
diff --git a/lib/galaxy_test/selenium/test_personal_information.py b/lib/galaxy_test/selenium/test_personal_information.py
index 4e5985814fc..74fdcb7e99b 100644
--- a/lib/galaxy_test/selenium/test_personal_information.py
+++ b/lib/galaxy_test/selenium/test_personal_information.py
@@ -135,7 +135,9 @@ class TestManageInformation(SeleniumTestCase):
element.send_keys(new_input_text)
def get_address_input_field(self, address_form, input_field_label):
- return address_form.find_element(self.by.CSS_SELECTOR, f"[data-label='{input_field_label}'] > input")
+ return address_form.find_element(
+ self.by.CSS_SELECTOR, f"[data-label='{input_field_label}'] > div > div > input"
+ )
class TestDeleteCurrentAccount(SeleniumTestCase):
diff --git a/lib/galaxy_test/selenium/test_workflow_editor.py b/lib/galaxy_test/selenium/test_workflow_editor.py
index f7de0383af0..88ef6c9c9ea 100644
--- a/lib/galaxy_test/selenium/test_workflow_editor.py
+++ b/lib/galaxy_test/selenium/test_workflow_editor.py
@@ -101,11 +101,8 @@ class TestWorkflowEditor(SeleniumTestCase, RunsWorkflows):
node = editor.node._(label="select_from_dataset_optional")
node.title.wait_for_and_click()
self.components.tool_form.parameter_checkbox(parameter="select_single").wait_for_and_click()
- # External (selenium-side) debounce hack for old backbone input
- # TODO: remove when form elements are all converted.
- self.components.tool_form.parameter_input(parameter="select_single").wait_for_and_send_keys("parameter valu")
- self.sleep_for(self.wait_types.UX_RENDER)
- self.components.tool_form.parameter_input(parameter="select_single").wait_for_and_send_keys("e")
+ self.components.tool_form.parameter_input(parameter="select_single").wait_for_and_send_keys("parameter value")
+ # onSetData does an extra POST to build_modules, so we need to wait for that ...
self.sleep_for(self.wait_types.UX_RENDER)
self.assert_workflow_has_changes_and_save()
workflow = self.workflow_populator.download_workflow(workflow_id)