Merge pull request #13375 from neoformit/tools-ui-refactor

Refactor _FieldText to vue.js
This commit is contained in:
Dannon
2023-01-09 10:36:36 -05:00
committed by GitHub
12 changed files with 370 additions and 17 deletions
@@ -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);
});
});
@@ -0,0 +1,91 @@
<template>
<div>
<b-form-checkbox v-model="currentStatus" class="ui-switch" switch>
Set value for this optional select field?
</b-form-checkbox>
<FormText
v-if="currentStatus"
:id="id"
v-model="currentValue"
:readonly="readonly"
:value="value"
:area="area"
:placeholder="placeholder"
:multiple="multiple"
:datalist="datalist"
:type="type" />
</div>
</template>
<script>
import FormText from "./FormText";
export default {
components: {
FormText,
},
props: {
value: {
default: "",
},
id: {
type: String,
default: null,
},
type: {
type: String,
default: "text",
},
area: {
// <textarea> instead of <input> element
type: Boolean,
default: false,
},
multiple: {
// Allow multiple entries to be created
type: Boolean,
default: false,
},
readonly: {
type: Boolean,
default: false,
},
placeholder: {
type: String,
default: null,
},
datalist: {
// Display list of suggestions in autocomplete dialog
type: Array,
default: null,
},
},
data() {
return {
status: false,
};
},
computed: {
currentValue: {
get() {
return this.value;
},
set(val) {
this.$emit("input", val);
},
},
currentStatus: {
get() {
return this.status || this.value !== null;
},
set(val) {
this.status = Boolean(val);
if (this.status) {
this.currentValue = "";
} else {
this.currentValue = null;
}
},
},
},
};
</script>
@@ -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("");
});
});
@@ -0,0 +1,118 @@
<template>
<b-row align-v="center">
<b-col>
<b-form-textarea
v-if="inputArea"
:id="id"
v-model="currentValue"
:class="['ui-text-area', cls]"
:readonly="readonly"
:placeholder="placeholder"
:style="style" />
<b-form-input
v-else
:id="id"
v-model="currentValue"
:class="['ui-input', cls]"
:readonly="readonly"
:placeholder="placeholder"
:style="style"
:type="acceptedTypes"
:list="`${id}-datalist`" />
<datalist v-if="datalist && !inputArea" :id="`${id}-datalist`">
<option v-for="data in datalist" :key="data.value" :label="data.label" :value="data.value" />
</datalist>
</b-col>
</b-row>
</template>
<script>
export default {
props: {
value: {
// String; Array for multiple
default: "",
},
id: {
type: String,
default: "",
},
type: {
type: String,
default: "text",
},
area: {
// <textarea> instead of <input> element
type: Boolean,
default: false,
},
multiple: {
// Allow multiple entries to be created
type: Boolean,
default: false,
},
readonly: {
type: Boolean,
default: false,
},
placeholder: {
type: String,
default: "",
},
color: {
type: String,
default: null,
},
cls: {
// Refers to an optional custom css class name
type: String,
default: null,
},
datalist: {
// Display list of suggestions in autocomplete dialog
type: Array,
default: null,
},
},
computed: {
acceptedTypes() {
return ["text", "password"].includes(this.type) ? this.type : "text";
},
currentValue: {
get() {
const v = this.value ?? "";
if (Array.isArray(v)) {
if (v.length === 0) {
return "";
}
return this.multiple
? this.value.reduce((str_value, v) => str_value + String(v) + "\n", "")
: String(this.value[0]);
}
return String(v);
},
set(newVal, oldVal) {
if (newVal !== oldVal) {
this.$emit("input", newVal);
}
},
},
inputArea() {
return this.area || this.multiple;
},
style() {
return this.color
? {
color: this.color,
"border-color": this.color,
}
: null;
},
},
};
</script>
<style scoped>
.ui-input-linked {
border-left-width: 0.5rem;
}
</style>
@@ -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 () => {
+30 -2
View File
@@ -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
</span>
</div>
<div v-if="showField" class="ui-form-field" :data-label="props.title">
<FormBoolean v-if="props.type === 'boolean'" :id="props.id" v-model="currentValue" />
<FormHidden v-else-if="isHiddenType" :id="props.id" v-model="currentValue" :info="attrs['info']" />
@@ -226,6 +227,33 @@ const isOptional = computed(() => !isRequired.value && attrs.value["optional"] !
:min="attrs.min"
:type="props.type ?? 'float'"
:workflow-building-mode="workflowBuildingMode" />
<FormOptionalText
v-else-if="props.type === 'select' && attrs.is_workflow && attrs.optional"
:id="id"
v-model="currentValue"
:readonly="attrs.readonly"
:value="attrs.value"
:area="attrs.area"
:placeholder="attrs.placeholder"
:multiple="attrs.multiple"
:datalist="attrs.datalist"
:type="props.type" />
<FormText
v-else-if="
['text', 'password'].includes(props.type) ||
(attrs.is_workflow && ['select', 'genomebuild', 'data_column', 'group_tag'].includes(props.type))
"
:id="id"
v-model="currentValue"
:readonly="attrs.readonly"
:value="attrs.value"
:area="attrs.area"
:placeholder="attrs.placeholder"
:color="attrs.color"
:multiple="attrs.multiple"
:cls="attrs.cls"
:datalist="attrs.datalist"
:type="props.type" />
<FormSelection
v-else-if="props.type === 'select' && ['radio', 'checkboxes'].includes(attrs.display)"
:id="id"
@@ -237,7 +265,7 @@ const isOptional = computed(() => !isRequired.value && attrs.value["optional"] !
:multiple="attrs.multiple" />
<FormColor v-else-if="props.type === 'color'" :id="props.id" v-model="currentValue" />
<FormDirectory v-else-if="props.type === 'directory_uri'" v-model="currentValue" />
<FormRulesEdit v-else-if="type == 'rules'" v-model="currentValue" :target="attrs.target" />
<FormRulesEdit v-else-if="props.type == 'rules'" v-model="currentValue" :target="attrs.target" />
<FormParameter
v-else-if="backbonejs"
:id="props.id"
@@ -78,7 +78,7 @@ export default {
},
nodeContentId: {
type: String,
required: true,
default: null,
},
nodeAnnotation: {
type: String,
@@ -84,7 +84,7 @@ export default {
},
nodeLabel: {
type: String,
required: true,
default: null,
},
nodeInputs: {
type: Array,
+2 -3
View File
@@ -100,7 +100,7 @@ export class WorkflowRunModel {
name: wp_name,
type: "text",
color: `hsl( ${++wp_count * 100}, 70%, 30% )`,
style: "ui-form-wp-source",
cls: "ui-input-linked",
links: [],
optional: true,
});
@@ -122,8 +122,7 @@ export class WorkflowRunModel {
wp_input.links.push(step);
input.wp_linked = true;
input.type = "text";
input.backdrop = true;
input.style = "ui-form-wp-target";
input.cls = "ui-input-linked";
});
});
_.each(step.replacement_parameters, (wp_name) => {
+1 -1
View File
@@ -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'
@@ -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):
@@ -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)