mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #15813 from assuntad23/12875/form_drilldown_vue_conversion
12875/form drilldown vue conversion
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, type ComputedRef } from "vue";
|
||||
import FormDrilldownList from "./FormDrilldownList.vue";
|
||||
import { getAllValues, type Option, type Value } from "./utilities";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
id: string;
|
||||
value?: Value;
|
||||
options: Array<Option>;
|
||||
multiple: boolean;
|
||||
}>(),
|
||||
{
|
||||
value: null,
|
||||
multiple: true,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", value: Value): void;
|
||||
}>();
|
||||
|
||||
const hasOptions = computed(() => {
|
||||
return props.options.length > 0;
|
||||
});
|
||||
|
||||
// Determine all available values
|
||||
const allValues: ComputedRef<string[]> = computed(() => {
|
||||
return getAllValues(props.options);
|
||||
});
|
||||
|
||||
// Determine current value
|
||||
const currentValue: ComputedRef<string[]> = computed(() => {
|
||||
if (props.value === null || props.value === "") {
|
||||
return [];
|
||||
} else if (Array.isArray(props.value)) {
|
||||
return props.value;
|
||||
} else {
|
||||
return [props.value];
|
||||
}
|
||||
});
|
||||
|
||||
// Determine if select all is checked
|
||||
const selectAllChecked: ComputedRef<boolean> = computed(() => {
|
||||
return allValues.value.length === currentValue.value.length;
|
||||
});
|
||||
|
||||
// Determine if select all state undetermined
|
||||
const selectAllIndeterminate: ComputedRef<boolean> = computed(() => {
|
||||
return ![0, allValues.value.length].includes(currentValue.value.length);
|
||||
});
|
||||
|
||||
// Handle click on individual check/radio element
|
||||
function handleClick(value: string): void {
|
||||
if (props.multiple) {
|
||||
const newValue: string[] = currentValue.value.slice();
|
||||
const index: number = newValue.indexOf(value);
|
||||
if (index !== -1) {
|
||||
newValue.splice(index, 1);
|
||||
} else {
|
||||
newValue.push(value);
|
||||
}
|
||||
if (newValue.length === 0) {
|
||||
emit("input", null);
|
||||
} else {
|
||||
emit("input", newValue);
|
||||
}
|
||||
} else {
|
||||
emit("input", value);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle click on select all checkbox to either select or unselect all values
|
||||
function onSelectAll(selected: boolean): void {
|
||||
emit("input", selected ? allValues.value : null);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasOptions">
|
||||
<b-form-checkbox
|
||||
v-if="multiple"
|
||||
v-localize
|
||||
:checked="selectAllChecked"
|
||||
:indeterminate="selectAllIndeterminate"
|
||||
class="d-inline select-all-checkbox"
|
||||
@change="onSelectAll">
|
||||
Select/Unselect All
|
||||
</b-form-checkbox>
|
||||
<form-drilldown-list
|
||||
:multiple="multiple"
|
||||
:current-value="currentValue"
|
||||
:options="options"
|
||||
:handle-click="handleClick" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import FormDrilldownOption from "./FormDrilldownOption.vue";
|
||||
import type { Option } from "./utilities";
|
||||
|
||||
defineProps<{
|
||||
currentValue: string[];
|
||||
handleClick: Function;
|
||||
multiple: boolean;
|
||||
options: Array<Option>;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="option in options" :key="option.name" class="ui-drilldown">
|
||||
<form-drilldown-option
|
||||
:current-value="currentValue"
|
||||
:handle-click="handleClick"
|
||||
:multiple="multiple"
|
||||
:option="option" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, type ComputedRef } from "vue";
|
||||
import { BFormCheckbox, BFormRadio } from "bootstrap-vue";
|
||||
import { getAllValues, type Option } from "./utilities";
|
||||
import FormDrilldownList from "./FormDrilldownList.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
currentValue: string[];
|
||||
option: Option;
|
||||
handleClick: Function;
|
||||
multiple: boolean;
|
||||
}>();
|
||||
|
||||
const showChildren = ref(false);
|
||||
|
||||
const hasOptions: ComputedRef<boolean> = computed(() => {
|
||||
return props.option.options.length > 0;
|
||||
});
|
||||
|
||||
// indicates if the value has been selected or not
|
||||
const isChecked: ComputedRef<boolean> = computed(() => {
|
||||
const optionValue = props.option.value;
|
||||
return props.currentValue.includes(optionValue);
|
||||
});
|
||||
|
||||
// determine required input element type
|
||||
const isComponent = computed(() => {
|
||||
return props.multiple ? BFormCheckbox : BFormRadio;
|
||||
});
|
||||
|
||||
function toggleChildren(): void {
|
||||
showChildren.value = !showChildren.value;
|
||||
}
|
||||
|
||||
function toggleInitialization(): void {
|
||||
const childValues = getAllValues(props.option.options);
|
||||
for (const childValue of childValues) {
|
||||
if (props.currentValue.includes(childValue)) {
|
||||
showChildren.value = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
toggleInitialization();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<b-button v-if="hasOptions" variant="link" class="btn p-0" @click="toggleChildren">
|
||||
<i v-if="showChildren" class="fa fa-minus-square" />
|
||||
<i v-else class="fa fa-plus-square" />
|
||||
</b-button>
|
||||
<component
|
||||
:is="isComponent"
|
||||
class="drilldown-option d-inline"
|
||||
value="true"
|
||||
:checked="isChecked"
|
||||
@change="handleClick(option.value)">
|
||||
{{ option.name }}
|
||||
</component>
|
||||
<form-drilldown-list
|
||||
v-if="hasOptions"
|
||||
v-show="showChildren"
|
||||
class="pl-5"
|
||||
:current-value="currentValue"
|
||||
:multiple="multiple"
|
||||
:options="option.options"
|
||||
:handle-click="handleClick" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,66 @@
|
||||
import { getAllValues } from "./utilities";
|
||||
|
||||
describe("drilldown component utilities", () => {
|
||||
it("get all values", () => {
|
||||
const options = [
|
||||
{
|
||||
value: "a",
|
||||
options: [
|
||||
{
|
||||
value: "aa",
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
value: "ab",
|
||||
options: [
|
||||
{
|
||||
value: "aba",
|
||||
options: [
|
||||
{
|
||||
value: "abaa",
|
||||
options: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: "abb",
|
||||
options: [
|
||||
{
|
||||
value: "abba",
|
||||
options: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: "b",
|
||||
options: [
|
||||
{
|
||||
value: "ba",
|
||||
options: [],
|
||||
},
|
||||
{
|
||||
value: "bb",
|
||||
options: [
|
||||
{
|
||||
value: "bba",
|
||||
options: [
|
||||
{
|
||||
value: "bbaa",
|
||||
options: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
expect(String(getAllValues(options))).toBe(
|
||||
String(["a", "b", "ba", "bb", "bba", "bbaa", "aa", "ab", "aba", "abb", "abba", "abaa"])
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
export interface Option {
|
||||
name: string;
|
||||
value: string;
|
||||
options: Array<Option>;
|
||||
}
|
||||
|
||||
export type Value = string[] | string | null;
|
||||
|
||||
/**
|
||||
* Returns an array of values from nested drill down options provided
|
||||
* @param headOptions Array of options to get values from
|
||||
* @returns values: string[]
|
||||
*/
|
||||
export function getAllValues(headOptions: Array<Option>): string[] {
|
||||
let options = null;
|
||||
const values: string[] = [];
|
||||
const stack: Array<Array<Option>> = [headOptions];
|
||||
while ((options = stack.pop())) {
|
||||
options.forEach((option) => {
|
||||
if (option.value) {
|
||||
values.push(option.value);
|
||||
}
|
||||
if (option.options.length > 0) {
|
||||
stack.push(option.options);
|
||||
}
|
||||
});
|
||||
}
|
||||
return values;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import FormText from "./Elements/FormText.vue";
|
||||
import FormOptionalText from "./Elements/FormOptionalText.vue";
|
||||
import FormRulesEdit from "./Elements/FormRulesEdit.vue";
|
||||
import FormUpload from "./Elements/FormUpload.vue";
|
||||
import FormDrilldown from "./Elements/FormDrilldown/FormDrilldown.vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { ref, computed, useAttrs } from "vue";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
@@ -263,6 +264,12 @@ const isOptional = computed(() => !isRequired.value && attrs.value["optional"] !
|
||||
:options="attrs.options"
|
||||
:optional="attrs.optional"
|
||||
:multiple="attrs.multiple" />
|
||||
<FormDrilldown
|
||||
v-else-if="props.type === 'drill_down'"
|
||||
:id="id"
|
||||
v-model="currentValue"
|
||||
:options="attrs.options"
|
||||
: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" />
|
||||
<FormUpload v-else-if="props.type === 'upload'" v-model="currentValue" />
|
||||
|
||||
@@ -29,10 +29,12 @@ import {
|
||||
faList,
|
||||
faLink,
|
||||
faLock,
|
||||
faMinusSquare,
|
||||
faPause,
|
||||
faPen,
|
||||
faPlay,
|
||||
faPlus,
|
||||
faPlusSquare,
|
||||
faQuestion,
|
||||
faShareAlt,
|
||||
faSitemap,
|
||||
@@ -75,10 +77,12 @@ library.add(
|
||||
faLink,
|
||||
faList,
|
||||
faLock,
|
||||
faMinusSquare,
|
||||
faPause,
|
||||
faPen,
|
||||
faPlay,
|
||||
faPlus,
|
||||
faPlusSquare,
|
||||
faQuestion,
|
||||
faShareAlt,
|
||||
faSitemap,
|
||||
|
||||
@@ -519,6 +519,10 @@ tool_form:
|
||||
reference: '.formatted-reference'
|
||||
about: '.tool-footer'
|
||||
storage_options: '.tool-storage'
|
||||
drilldown_select_all: 'div.ui-form-element[id="form-element-${parameter}"] div.select-all-checkbox'
|
||||
drilldown_option: '.drilldown-option'
|
||||
drilldown_expand: '.fa-plus-square'
|
||||
|
||||
|
||||
labels:
|
||||
generate_tour: 'Generate Tour'
|
||||
|
||||
@@ -504,7 +504,7 @@ class UsesHistoryItemAssertions(NavigatesGalaxyMixin):
|
||||
def assert_item_peek_includes(self, hid, expected):
|
||||
item_body = self.history_panel_item_component(hid=hid)
|
||||
peek_text = item_body.peek.wait_for_text()
|
||||
assert expected in peek_text
|
||||
assert expected in peek_text, f"Expected peek [{expected}] not found in peek [{peek_text}]"
|
||||
|
||||
def assert_item_info_includes(self, hid, expected):
|
||||
item_body = self.history_panel_item_component(hid=hid)
|
||||
|
||||
@@ -74,6 +74,26 @@ class TestToolForm(SeleniumTestCase, UsesHistoryItemAssertions):
|
||||
generic_item.find_element(By.CSS_SELECTOR, "[title='Run Job Again']").click()
|
||||
self.components.tool_form.execute.wait_for_visible()
|
||||
|
||||
@selenium_test
|
||||
def test_drilldown_tool(self):
|
||||
self._open_drilldown_test_tool()
|
||||
# click first option in first drilldown component
|
||||
self.wait_for_and_click(self.components.tool_form.drilldown_expand)
|
||||
self.wait_for_and_click(self.components.tool_form.drilldown_option)
|
||||
# click select all in second drilldown component
|
||||
self.wait_for_and_click(self.components.tool_form.drilldown_select_all(parameter="dd_recurse"))
|
||||
self.tool_form_execute()
|
||||
self.history_panel_wait_for_hid_ok(1)
|
||||
# click hid 1 in history panel
|
||||
self.history_panel_click_item_title(hid=1)
|
||||
# assert that the dataset peek is d = a dd_recurse = a,b,c
|
||||
self.assert_item_peek_includes(1, "dd a")
|
||||
self.assert_item_peek_includes(1, "dd_recurse aa,aba,abb,ba,bba,bbb")
|
||||
|
||||
def _open_drilldown_test_tool(self):
|
||||
self.home()
|
||||
self.tool_open("drill_down")
|
||||
|
||||
@staticmethod
|
||||
def click_menu_item(menu, text):
|
||||
for element in menu.find_elements(By.CSS_SELECTOR, "a"):
|
||||
|
||||
Reference in New Issue
Block a user