Skip to content

Commit

Permalink
Merge pull request galaxyproject#17021 from ahmedhamidawan/show_tool_…
Browse files Browse the repository at this point in the history
…panel_view_on_panel

Show current tool panel view name on top of tool panel
  • Loading branch information
martenson authored Nov 15, 2023
2 parents 706e977 + 4e24d56 commit 6b3b35d
Show file tree
Hide file tree
Showing 7 changed files with 292 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
<b-dropdown
v-b-tooltip.hover.top.noninteractive
right
block
no-caret
title="Show panel options"
variant="link"
toggle-class="text-decoration-none"
role="menu"
aria-label="View all tool panel configurations"
class="tool-panel-dropdown"
class="tool-panel-dropdown w-100"
size="sm">
<template v-slot:button-content>
<span class="sr-only">View all tool panel configurations</span>
<slot name="panel-view-selector"></slot><span class="sr-only">View all tool panel configurations</span>
</template>
<PanelViewMenuItem
:current-panel-view="currentPanelView"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,7 @@
</template>

<script>
const types_to_icons = {
default: "undo",
generic: "filter",
ontology: "sitemap",
activity: "project-diagram",
publication: "newspaper",
training: "graduation-cap",
};
import { types_to_icons } from "../utilities";
export default {
props: {
Expand Down
100 changes: 100 additions & 0 deletions client/src/components/Panels/ToolPanel.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import "jest-location-mock";

import { mount } from "@vue/test-utils";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
import toolsList from "components/ToolsView/testData/toolsList";
import toolsListInPanel from "components/ToolsView/testData/toolsListInPanel";
import flushPromises from "flush-promises";
import { createPinia } from "pinia";
import { getLocalVue } from "tests/jest/helpers";

import { useConfig } from "@/composables/config";

import viewsList from "./testData/viewsList";
import ToolPanel from "./ToolPanel";
import { types_to_icons } from "./utilities";

const localVue = getLocalVue();

const TEST_PANELS_URI = "/api/tool_panels";

jest.mock("composables/config");
useConfig.mockReturnValue({
config: {},
isConfigLoaded: true,
});

describe("ToolPanel", () => {
it("test navigation of tool panel views menu", async () => {
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 });

const pinia = createPinia();
const wrapper = mount(ToolPanel, {
propsData: {
workflow: false,
editorWorkflows: null,
dataManagers: null,
moduleSections: null,
},
localVue,
stubs: {
icon: { template: "<div></div>" },
ToolBox: true,
},
pinia,
});

await flushPromises();

// 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");
await wrapper.find("#toolbox-heading").trigger("click");
await flushPromises();

const dropdownMenu = wrapper.find(".dropdown-menu.show");
expect(dropdownMenu.exists()).toBe(true);

// Test: check if the dropdown menu has all the panel views
const dropdownItems = dropdownMenu.findAll(".dropdown-item");
expect(dropdownItems.length).toEqual(Object.keys(viewsList).length);

// Test: click on each panel view, and check if the panel view is changed
for (const [key, value] of Object.entries(viewsList)) {
// find dropdown item
const currItem = dropdownMenu.find(`[data-panel-id='${key}']`);
if (key !== "default") {
// Test: check if the panel view has appropriate description
const description = currItem.attributes().title || null;
expect(description).toBe(value.description);

// set current panel view
await currItem.trigger("click");
await flushPromises();

// Test: check if the current panel view is selected now
expect(currItem.find(".fa-check").exists()).toBe(true);

// Test: check if the panel header now has an icon and a changed name
const panelViewIcon = wrapper.find("[data-description='panel view header icon']");
expect(panelViewIcon.classes()).toContain(`fa-${types_to_icons[value.view_type]}`);
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);
expect(wrapper.find("[data-description='panel view header icon']").exists()).toBe(false);
}
}
});
});
102 changes: 79 additions & 23 deletions client/src/components/Panels/ToolPanel.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faCaretDown } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import axios from "axios";
import { storeToRefs } from "pinia";
import { onMounted, ref, watch } from "vue";
import { computed, onMounted, ref, watch } from "vue";
import { getAppRoot } from "@/onload";
import { useToolStore } from "@/stores/toolStore";
import { type PanelView, useToolStore } from "@/stores/toolStore";
import localize from "@/utils/localization";
import { types_to_icons } from "./utilities";
import LoadingSpan from "../LoadingSpan.vue";
import FavoritesButton from "./Buttons/FavoritesButton.vue";
import PanelViewButton from "./Buttons/PanelViewButton.vue";
import PanelViewMenu from "./Menus/PanelViewMenu.vue";
import ToolBox from "./ToolBox.vue";
import Heading from "@/components/Common/Heading.vue";
library.add(faCaretDown);
const props = defineProps({
workflow: { type: Boolean, default: false },
editorWorkflows: { type: Array, default: null },
Expand All @@ -33,7 +40,7 @@ const toolStore = useToolStore();
const { currentPanelView, isPanelPopulated } = storeToRefs(toolStore);
const query = ref("");
const panelViews = ref(null);
const panelViews = ref<Record<string, PanelView> | null>(null);
const showAdvanced = ref(false);
onMounted(async () => {
Expand Down Expand Up @@ -70,6 +77,35 @@ watch(
}
);
const toolPanelHeader = computed(() => {
if (showAdvanced.value) {
return localize("Advanced Tool Search");
} else if (
currentPanelView.value !== "default" &&
panelViews.value &&
panelViews.value[currentPanelView.value]?.name
) {
return localize(panelViews.value[currentPanelView.value]?.name);
} else {
return localize("Tools");
}
});
const viewIcon = computed(() => {
if (showAdvanced.value) {
return "search";
} else if (
currentPanelView.value !== "default" &&
panelViews.value &&
typeof panelViews.value[currentPanelView.value]?.view_type === "string"
) {
const viewType = panelViews.value[currentPanelView.value]?.view_type;
return viewType ? types_to_icons[viewType] : null;
} else {
return null;
}
});
async function initializeTools() {
try {
await toolStore.fetchTools();
Expand Down Expand Up @@ -103,25 +139,37 @@ function onInsertWorkflowSteps(workflowId: string, workflowStepCount: number | u
<template>
<div v-if="arePanelsFetched" class="unified-panel" aria-labelledby="toolbox-heading">
<div unselectable="on">
<div class="unified-panel-header-inner">
<nav class="d-flex justify-content-between mx-3 my-2">
<Heading v-if="!showAdvanced" id="toolbox-heading" h2 inline size="sm">{{
localize("Tools")
}}</Heading>
<Heading v-else id="toolbox-heading" h2 inline size="sm">{{
localize("Advanced Tool Search")
}}</Heading>
<div class="panel-header-buttons">
<b-button-group>
<FavoritesButton v-if="!showAdvanced" :query="query" @onFavorites="(q) => (query = q)" />
<PanelViewButton
v-if="panelViews && Object.keys(panelViews).length > 1"
:panel-views="panelViews"
:current-panel-view="currentPanelView"
@updatePanelView="updatePanelView" />
</b-button-group>
</div>
</nav>
<div class="unified-panel-header-inner mx-3 my-2 d-flex justify-content-between">
<PanelViewMenu
v-if="panelViews && Object.keys(panelViews).length > 1"
:panel-views="panelViews"
:current-panel-view="currentPanelView"
@updatePanelView="updatePanelView">
<template v-slot:panel-view-selector>
<div class="d-flex justify-content-between panel-view-selector">
<div>
<span
v-if="viewIcon"
:class="['fas', `fa-${viewIcon}`, 'mr-1']"
data-description="panel view header icon" />
<Heading
id="toolbox-heading"
:class="!showAdvanced && toolPanelHeader !== 'Tools' && 'font-italic'"
h2
inline
size="sm"
>{{ toolPanelHeader }}
</Heading>
</div>
<div v-if="!showAdvanced" class="panel-header-buttons">
<FontAwesomeIcon icon="caret-down" />
</div>
</div>
</template>
</PanelViewMenu>
<div v-if="!showAdvanced" class="panel-header-buttons">
<FavoritesButton :query="query" @onFavorites="(q) => (query = q)" />
</div>
</div>
</div>
<ToolBox
Expand All @@ -145,3 +193,11 @@ function onInsertWorkflowSteps(workflowId: string, workflowStepCount: number | u
</div>
</div>
</template>

<style lang="scss" scoped>
@import "theme/blue.scss";
.panel-view-selector {
color: $panel-header-text-color;
}
</style>
74 changes: 74 additions & 0 deletions client/src/components/Panels/testData/viewsList.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"default": {
"id": "default",
"model_class": "type",
"name": "Full Tool Panel",
"description": "Galaxy's fully configured toolbox panel with all sections, tools, and configured workflows loaded.",
"view_type": "default",
"searchable": true
},
"ontology:edam_operations": {
"id": "ontology:edam_operations",
"model_class": "EdamToolPanelView",
"name": "EDAM Operations",
"description": "Tools are grouped using annotated EDAM operation information (if available).",
"view_type": "ontology",
"searchable": true
},
"ontology:edam_topics": {
"id": "ontology:edam_topics",
"model_class": "EdamToolPanelView",
"name": "EDAM Topics",
"description": "Tools are grouped using annotated EDAM topic information (if available).",
"view_type": "ontology",
"searchable": true
},
"activity_1": {
"id": "activity_1",
"model_class": "StaticToolPanelView",
"name": "Globally Applied Filters",
"description": null,
"view_type": "activity",
"searchable": true
},
"generic_1": {
"id": "generic_1",
"model_class": "StaticToolPanelView",
"name": "Custom Panel in a New Section",
"description": null,
"view_type": "generic",
"searchable": true
},
"activity_2": {
"id": "activity_2",
"model_class": "StaticToolPanelView",
"name": "Filtering Tools",
"description": null,
"view_type": "activity",
"searchable": true
},
"publication_1": {
"id": "publication_1",
"model_class": "StaticToolPanelView",
"name": "Merged Section Example",
"description": null,
"view_type": "publication",
"searchable": true
},
"training_1": {
"id": "training_1",
"model_class": "StaticToolPanelView",
"name": "Filtered Test Section",
"description": null,
"view_type": "training",
"searchable": true
},
"generic_2": {
"id": "generic_2",
"model_class": "StaticToolPanelView",
"name": "Merged Section Example",
"description": null,
"view_type": "generic",
"searchable": true
}
}
10 changes: 10 additions & 0 deletions client/src/components/Panels/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ interface SearchMatch {
order: number;
}

/** Returns icon for tool panel `view_type` */
export const types_to_icons = {
default: "undo",
generic: "filter",
ontology: "sitemap",
activity: "project-diagram",
publication: "newspaper",
training: "graduation-cap",
};

// Converts filterSettings { key: value } to query = "key:value"
export function createWorkflowQuery(filterSettings: Record<string, string | boolean>) {
let query = "";
Expand Down
Loading

0 comments on commit 6b3b35d

Please sign in to comment.