diff --git a/client/src/components/BaseComponents/GDropdown.vue b/client/src/components/BaseComponents/GDropdown.vue new file mode 100644 index 000000000000..f847769e0ae5 --- /dev/null +++ b/client/src/components/BaseComponents/GDropdown.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/client/src/components/BaseComponents/GDropdownDivider.vue b/client/src/components/BaseComponents/GDropdownDivider.vue new file mode 100644 index 000000000000..f49f0ea94b06 --- /dev/null +++ b/client/src/components/BaseComponents/GDropdownDivider.vue @@ -0,0 +1,5 @@ + + + diff --git a/client/src/components/BaseComponents/GDropdownForm.vue b/client/src/components/BaseComponents/GDropdownForm.vue new file mode 100644 index 000000000000..690412999a9c --- /dev/null +++ b/client/src/components/BaseComponents/GDropdownForm.vue @@ -0,0 +1,24 @@ + + + diff --git a/client/src/components/BaseComponents/GDropdownGroup.vue b/client/src/components/BaseComponents/GDropdownGroup.vue new file mode 100644 index 000000000000..fd599b6fe1b5 --- /dev/null +++ b/client/src/components/BaseComponents/GDropdownGroup.vue @@ -0,0 +1,22 @@ + + + diff --git a/client/src/components/BaseComponents/GDropdownItem.test.ts b/client/src/components/BaseComponents/GDropdownItem.test.ts new file mode 100644 index 000000000000..4b4db7cfb245 --- /dev/null +++ b/client/src/components/BaseComponents/GDropdownItem.test.ts @@ -0,0 +1,58 @@ +import { getLocalVue } from "@tests/vitest/helpers"; +import { mount } from "@vue/test-utils"; +import { describe, expect, it } from "vitest"; + +import GDropdownItem from "./GDropdownItem.vue"; + +const localVue = getLocalVue(true); + +/** Click the item and report whether the navigation was cancelled. */ +async function clickAndCheckDefaultPrevented(props: object) { + const wrapper = mount(GDropdownItem as object, { + propsData: props, + slots: { default: "Item" }, + localVue, + }); + const event = new MouseEvent("click", { bubbles: true, cancelable: true }); + wrapper.get("a").element.dispatchEvent(event); + await wrapper.vm.$nextTick(); + return { wrapper, defaultPrevented: event.defaultPrevented }; +} + +describe("GDropdownItem.vue", () => { + it("cancels navigation for action items, which fall back to href='#'", async () => { + const { wrapper, defaultPrevented } = await clickAndCheckDefaultPrevented({}); + + expect(wrapper.get("a").attributes("href")).toBe("#"); + expect(defaultPrevented).toBe(true); + expect(wrapper.emitted("click")).toHaveLength(1); + }); + + it("cancels navigation when href='#' is passed explicitly", async () => { + const { wrapper, defaultPrevented } = await clickAndCheckDefaultPrevented({ href: "#" }); + + expect(defaultPrevented).toBe(true); + expect(wrapper.emitted("click")).toHaveLength(1); + }); + + it("lets a real href navigate so target='_blank' can open a new tab", async () => { + const { wrapper, defaultPrevented } = await clickAndCheckDefaultPrevented({ + href: "https://example.org/workflow.ga", + target: "_blank", + }); + + expect(wrapper.get("a").attributes("target")).toBe("_blank"); + expect(defaultPrevented).toBe(false); + expect(wrapper.emitted("click")).toHaveLength(1); + }); + + it("cancels navigation and emits nothing when disabled", async () => { + const { wrapper, defaultPrevented } = await clickAndCheckDefaultPrevented({ + href: "https://example.org/workflow.ga", + disabled: true, + }); + + expect(defaultPrevented).toBe(true); + expect(wrapper.emitted("click")).toBeUndefined(); + }); +}); diff --git a/client/src/components/BaseComponents/GDropdownItem.vue b/client/src/components/BaseComponents/GDropdownItem.vue new file mode 100644 index 000000000000..283d53526195 --- /dev/null +++ b/client/src/components/BaseComponents/GDropdownItem.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/client/src/components/BaseComponents/GDropdownItemButton.vue b/client/src/components/BaseComponents/GDropdownItemButton.vue new file mode 100644 index 000000000000..f22e9c7f3312 --- /dev/null +++ b/client/src/components/BaseComponents/GDropdownItemButton.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/client/src/components/BaseComponents/GDropdownText.vue b/client/src/components/BaseComponents/GDropdownText.vue new file mode 100644 index 000000000000..35917d3977d6 --- /dev/null +++ b/client/src/components/BaseComponents/GDropdownText.vue @@ -0,0 +1,7 @@ + + + diff --git a/client/src/components/Collections/common/PairingFilterInputGroup.vue b/client/src/components/Collections/common/PairingFilterInputGroup.vue index 3f35403e3e5c..899708f85233 100644 --- a/client/src/components/Collections/common/PairingFilterInputGroup.vue +++ b/client/src/components/Collections/common/PairingFilterInputGroup.vue @@ -1,9 +1,12 @@ - + Download Dataset - + - Download {{ metaFile.file_type }} - - + + import { faBurn, faCog, faEyeSlash, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; -import { BDropdown, BDropdownItem } from "bootstrap-vue"; import { toRef } from "vue"; import type { HistorySummaryExtended } from "@/api"; @@ -13,6 +12,9 @@ import { import { useConfirmDialog } from "@/composables/confirmDialog"; import { useHistoryContentStats } from "@/composables/historyContentStats"; +import GDropdown from "@/components/BaseComponents/GDropdown.vue"; +import GDropdownItem from "@/components/BaseComponents/GDropdownItem.vue"; + interface Props { history: HistorySummaryExtended; } @@ -73,7 +75,7 @@ async function runOperation(operation: () => Promise) { - + Unhide All Hidden Content - + - + Delete All Hidden Content - + - + Purge All Deleted Content - - + + diff --git a/client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.vue b/client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.vue index b0bd3ed5c9db..2662f879d68a 100644 --- a/client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.vue +++ b/client/src/components/History/CurrentHistory/HistoryOperations/SelectionOperations.vue @@ -1,6 +1,6 @@ - from History + from History - + from User Directory - + - + - - + + from Import Directory - + - + from Path - - - + + + - + - + as Datasets - + - + as a Collection - - + +
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; -import { BDropdownItem, BNavItemDropdown } from "bootstrap-vue"; -import { type PropType, ref } from "vue"; +import { BNavItemDropdown } from "bootstrap-vue"; +import { type PropType, provide, ref } from "vue"; import type { IconLike } from "@/components/icons/galaxyIcons"; +import GDropdownItem from "@/components/BaseComponents/GDropdownItem.vue"; import TextShort from "@/components/Common/TextShort.vue"; -const dropdown = ref(null); +const dropdown = ref>(); + +provide("g-dropdown-hide", () => { + dropdown.value?.hide(); +}); interface MenuItem { title: string; @@ -47,7 +52,7 @@ defineProps({ diff --git a/client/src/components/Panels/Menus/PanelViewMenu.vue b/client/src/components/Panels/Menus/PanelViewMenu.vue index 279ff685a322..b7598528afb1 100644 --- a/client/src/components/Panels/Menus/PanelViewMenu.vue +++ b/client/src/components/Panels/Menus/PanelViewMenu.vue @@ -1,7 +1,6 @@