diff --git a/client/src/components/Help/terms.yml b/client/src/components/Help/terms.yml index c361ddf91e60..395fdf7ba708 100644 --- a/client/src/components/Help/terms.yml +++ b/client/src/components/Help/terms.yml @@ -145,3 +145,5 @@ galaxy: A utility for uploading files to a Galaxy server from the command line. Use ``galaxy-upload`` to upload file(s) to a Galaxy server, and ``galaxy-history-search``, a helper utility to find Galaxy histories to pass to the ``galaxy-upload`` command. + ruleBased: | + Galaxy can bulk import lists & tables of URLs into datasets or collections using reproducible rules. diff --git a/client/src/components/Masthead/Masthead.vue b/client/src/components/Masthead/Masthead.vue index 69b1d6cd9624..61101f9a5024 100644 --- a/client/src/components/Masthead/Masthead.vue +++ b/client/src/components/Masthead/Masthead.vue @@ -191,7 +191,7 @@ onMounted(() => { position: absolute; left: 1.6rem; top: 1.6rem; - font-size: 0.4rem; + font-size: 0.6rem; font-weight: bold; } } diff --git a/client/src/components/Panels/ToolPanel.test.ts b/client/src/components/Panels/ToolPanel.test.ts index 1541fa67be4e..d7775e48cc58 100644 --- a/client/src/components/Panels/ToolPanel.test.ts +++ b/client/src/components/Panels/ToolPanel.test.ts @@ -6,18 +6,32 @@ import MockAdapter from "axios-mock-adapter"; import flushPromises from "flush-promises"; import { createPinia } from "pinia"; import { getLocalVue } from "tests/jest/helpers"; +import { ref } from "vue"; import toolsList from "@/components/ToolsView/testData/toolsList.json"; import toolsListInPanel from "@/components/ToolsView/testData/toolsListInPanel.json"; +import { useUserLocalStorage } from "@/composables/userLocalStorage"; +import { useToolStore } from "@/stores/toolStore"; -import viewsList from "./testData/viewsList.json"; +import viewsListJson from "./testData/viewsList.json"; import { types_to_icons } from "./utilities"; import ToolPanel from "./ToolPanel.vue"; +interface ToolPanelView { + id: string; + model_class: string; + name: string; + description: string | null; + view_type: string; + searchable: boolean; +} + const localVue = getLocalVue(); const TEST_PANELS_URI = "/api/tool_panels"; +const DEFAULT_VIEW_ID = "default"; +const PANEL_VIEW_ERR_MSG = "Error loading panel view"; jest.mock("@/composables/config", () => ({ useConfig: jest.fn(() => ({ @@ -26,16 +40,59 @@ jest.mock("@/composables/config", () => ({ })), })); +jest.mock("@/composables/userLocalStorage", () => ({ + useUserLocalStorage: jest.fn(() => ref(DEFAULT_VIEW_ID)), +})); + describe("ToolPanel", () => { - it("test navigation of tool panel views menu", async () => { + const viewsList = viewsListJson as Record; + + /** Mocks and stores a non-default panel view as the current panel view */ + function storeNonDefaultView() { + // find a view in object viewsList that is not DEFAULT_VIEW_ID + const viewKey = Object.keys(viewsList).find((id) => id !== DEFAULT_VIEW_ID); + if (!viewKey) { + throw new Error("No non-default view found in viewsList"); + } + const view = viewsList[viewKey]; + if (!view) { + throw new Error(`View with key ${viewKey} not found in viewsList`); + } + // ref and useUserLocalStorage are already imported at the top + (useUserLocalStorage as jest.Mock).mockImplementation(() => ref(viewKey)); + return { viewKey, view }; + } + + /** + * Sets up wrapper for ToolPanel component + * @param {String} errorView If provided, we mock an error for this view + * @param {Boolean} failDefault If true and error view is provided, we + * mock an error for the default view as well + * @returns wrapper + */ + async function createWrapper(errorView = "", failDefault = false) { const axiosMock = new MockAdapter(axios); axiosMock - .onGet(/\/api\/tool_panels\/.*/) - .reply(200, toolsListInPanel) .onGet(`/api/tools?in_panel=False`) .replyOnce(200, toolsList) .onGet(TEST_PANELS_URI) - .reply(200, { default_panel_view: "default", views: viewsList }); + .reply(200, { default_panel_view: DEFAULT_VIEW_ID, views: viewsList }); + + if (errorView) { + axiosMock.onGet(`/api/tool_panels/${errorView}`).reply(400, { err_msg: PANEL_VIEW_ERR_MSG }); + if (errorView !== DEFAULT_VIEW_ID && !failDefault) { + axiosMock.onGet(`/api/tool_panels/${DEFAULT_VIEW_ID}`).reply(200, toolsListInPanel); + } else if (failDefault) { + axiosMock.onGet(`/api/tool_panels/${DEFAULT_VIEW_ID}`).reply(400, { err_msg: PANEL_VIEW_ERR_MSG }); + } + } else { + // mock response for all panel views + axiosMock.onGet(/\/api\/tool_panels\/.*/).reply(200, toolsListInPanel); + } + + // setting this because for the default view, we just show "Tools" as the name + // even though the backend returns "Full Tool Panel" + viewsList[DEFAULT_VIEW_ID]!.name = "Tools"; const pinia = createPinia(); const wrapper = mount(ToolPanel as object, { @@ -55,12 +112,17 @@ describe("ToolPanel", () => { await flushPromises(); + return { wrapper }; + } + + it("test navigation of tool panel views menu", async () => { + const { wrapper } = await createWrapper(); // there is a panel view selector initially collapsed expect(wrapper.find(".panel-view-selector").exists()).toBe(true); expect(wrapper.find(".dropdown-menu.show").exists()).toBe(false); // Test: starts up with a default panel view, click to open menu - expect(wrapper.find("#toolbox-heading").text()).toBe("Tools"); + expect(wrapper.find("#toolbox-heading").text()).toBe(viewsList[DEFAULT_VIEW_ID]!.name); await wrapper.find("#toolbox-heading").trigger("click"); await flushPromises(); @@ -75,7 +137,7 @@ describe("ToolPanel", () => { for (const [key, value] of Object.entries(viewsList)) { // find dropdown item const currItem = dropdownMenu.find(`[data-panel-id='${key}']`); - if (key !== "default") { + if (key !== DEFAULT_VIEW_ID) { // Test: check if the panel view has appropriate description const description = currItem.attributes().title || null; expect(description).toBe(value.description); @@ -92,7 +154,7 @@ describe("ToolPanel", () => { expect(panelViewIcon.classes()).toContain( `fa-${types_to_icons[value.view_type as keyof typeof types_to_icons]}` ); - expect(wrapper.find("#toolbox-heading").text()).toBe(value.name); + expect(wrapper.find("#toolbox-heading").text()).toBe(value!.name); } else { // Test: check if the default panel view is already selected, and no icon expect(currItem.find(".fa-check").exists()).toBe(true); @@ -100,4 +162,42 @@ describe("ToolPanel", () => { } } }); + + it("initializes non default current panel view correctly", async () => { + const { viewKey, view } = storeNonDefaultView(); + + const { wrapper } = await createWrapper(); + + // starts up with a non default panel view + expect(wrapper.find("#toolbox-heading").text()).toBe(view!.name); + const toolStore = useToolStore(); + expect(toolStore.currentPanelView).toBe(viewKey); + }); + + it("changes panel to default if current panel view throws error", async () => { + const { viewKey, view } = storeNonDefaultView(); + + const { wrapper } = await createWrapper(viewKey); + + // does not initialize non default panel view, and changes to default + expect(wrapper.find("#toolbox-heading").text()).not.toBe(view!.name); + expect(wrapper.find("#toolbox-heading").text()).toBe(viewsList[DEFAULT_VIEW_ID]!.name); + const toolStore = useToolStore(); + expect(toolStore.currentPanelView).toBe(DEFAULT_VIEW_ID); + + // toolbox loaded + expect(wrapper.find('[data-description="panel toolbox"]').exists()).toBe(true); + }); + + it("simply shows error if even default panel view throws error", async () => { + const { viewKey } = storeNonDefaultView(); + + const { wrapper } = await createWrapper(viewKey, true); + + // toolbox not loaded + expect(wrapper.find('[data-description="panel toolbox"]').exists()).toBe(false); + + // error message shown + expect(wrapper.find('[data-description="tool panel error message"]').text()).toBe(PANEL_VIEW_ERR_MSG); + }); }); diff --git a/client/src/components/Panels/ToolPanel.vue b/client/src/components/Panels/ToolPanel.vue index 55bc38f28694..cf0fa3f4fcd5 100644 --- a/client/src/components/Panels/ToolPanel.vue +++ b/client/src/components/Panels/ToolPanel.vue @@ -2,10 +2,11 @@ import { faCaretDown } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { storeToRefs } from "pinia"; -import { computed, onMounted, ref, watch } from "vue"; +import { computed, ref, watch } from "vue"; import { useToolStore } from "@/stores/toolStore"; import localize from "@/utils/localization"; +import { errorMessageAsString, rethrowSimple } from "@/utils/simple-error"; import { types_to_icons } from "./utilities"; @@ -36,17 +37,20 @@ const { currentPanelView, defaultPanelView, isPanelPopulated, loading, panel, pa const loadingView = ref(undefined); const query = ref(""); const showAdvanced = ref(false); +const errorMessage = ref(undefined); -onMounted(async () => { +initializeToolPanel(); +async function initializeToolPanel() { try { await toolStore.fetchPanelViews(); await initializeTools(); } catch (error) { console.error(error); + errorMessage.value = errorMessageAsString(error); } finally { arePanelsFetched.value = true; } -}); +} watch( () => currentPanelView.value, @@ -117,6 +121,8 @@ async function initializeTools() { await toolStore.initCurrentPanelView(defaultPanelView.value); } catch (error: any) { console.error("ToolPanel - Intialize error:", error); + errorMessage.value = errorMessageAsString(error); + rethrowSimple(error); } } @@ -204,6 +210,11 @@ watch( @onInsertModule="onInsertModule" @onInsertWorkflow="onInsertWorkflow" @onInsertWorkflowSteps="onInsertWorkflowSteps" /> +
+ + {{ errorMessage }} + +
diff --git a/client/src/components/Upload/UploadModal.vue b/client/src/components/Upload/UploadModal.vue index f3b8a15907d8..350d2fb840b9 100644 --- a/client/src/components/Upload/UploadModal.vue +++ b/client/src/components/Upload/UploadModal.vue @@ -1,5 +1,5 @@