diff --git a/client/src/components/Panels/Common/ToolSection.vue b/client/src/components/Panels/Common/ToolSection.vue index 66e7d912cff5..1e3a668944ee 100644 --- a/client/src/components/Panels/Common/ToolSection.vue +++ b/client/src/components/Panels/Common/ToolSection.vue @@ -3,7 +3,7 @@ import { computed, onMounted, onUnmounted, ref, watch } from "vue"; import { eventHub } from "@/components/plugins/eventHub.js"; import { useConfig } from "@/composables/config"; -import { type Tool as ToolType } from "@/stores/toolStore"; +import { type Tool as ToolType, type ToolSectionLabel as LabelType } from "@/stores/toolStore"; import { useToolStore } from "@/stores/toolStore"; import { type Workflow } from "@/stores/workflowStore"; import ariaAlert from "@/utils/ariaAlert"; @@ -66,7 +66,15 @@ const elems = computed(() => { return props.category.elems; } if (props.category.tools !== undefined && props.category.tools.length > 0) { - return props.category.tools.map((toolId: string) => toolStore.getToolForId(toolId)); + return props.category.tools.map((toolId: string) => { + const tool = toolStore.getToolForId(toolId); + if (!tool && toolId.startsWith("panel_label_") && props.category.panel_labels) { + const labelId = toolId.split("panel_label_")[1]; + return props.category.panel_labels.find((label: LabelType) => label.id === labelId); + } else { + return tool; + } + }); } return []; }); diff --git a/client/src/components/Panels/ToolBox.vue b/client/src/components/Panels/ToolBox.vue index 21352e49483f..1680c29fc3da 100644 --- a/client/src/components/Panels/ToolBox.vue +++ b/client/src/components/Panels/ToolBox.vue @@ -9,7 +9,7 @@ import { useRouter } from "vue-router/composables"; import { getGalaxyInstance } from "@/app"; import { useGlobalUploadModal } from "@/composables/globalUploadModal"; import { getAppRoot } from "@/onload/loadConfig"; -import { type Tool, type ToolSection as ToolSectionType } from "@/stores/toolStore"; +import { type Tool, type ToolSection as ToolSectionType, type ToolSectionLabel } from "@/stores/toolStore"; import { useToolStore } from "@/stores/toolStore"; import { Workflow, type Workflow as WorkflowType } from "@/stores/workflowStore"; import localize from "@/utils/localization"; @@ -92,14 +92,24 @@ const dataManagerSection = computed(() => { /** `toolsById` from `toolStore`, except it only has valid tools for `props.workflow` value */ const localToolsById = computed(() => { - // Filter the items with is_compat === true + const addedToolTexts: string[] = []; if (toolStore.toolsById && Object.keys(toolStore.toolsById).length > 0) { - const toolEntries = Object.entries(toolStore.toolsById).filter( - ([_, tool]: [string, any]) => + const toolEntries = Object.entries(toolStore.toolsById).filter(([_, tool]: [string, any]) => { + // filter out duplicate tools (different ids, same exact name+description) + // related ticket: https://github.com/galaxyproject/galaxy/issues/16145 + const toolText: string = tool.name + tool.description; + if (addedToolTexts.includes(toolText)) { + return false; + } else { + addedToolTexts.push(toolText); + } + // filter on non-hidden, non-disabled, and workflow compatibile (based on props.workflow) + return ( !tool.hidden && tool.disabled !== true && (props.workflow ? tool.is_workflow_compatible : !SECTION_IDS_TO_EXCLUDE.includes(tool.panel_section_id)) - ); + ); + }); return Object.fromEntries(toolEntries); } return {}; @@ -111,7 +121,15 @@ const localSectionsById = computed(() => { // for all values that are `ToolSection`s, filter out tools that aren't in `localToolsById` const sectionEntries = Object.entries(currentPanel.value).map(([id, section]: [string, any]) => { if (section.tools && Array.isArray(section.tools)) { - section.tools = section.tools.filter((tool: string) => validToolIdsInCurrentView.includes(tool)); + section.tools = section.tools.filter((toolId: string) => { + if (validToolIdsInCurrentView.includes(toolId)) { + return true; + } else if (toolId.startsWith("panel_label_") && section.panel_labels) { + // panel_label_ is a special case where there is a label within a section + const labelId = toolId.split("panel_label_")[1]; + return section.panel_labels.find((label: ToolSectionLabel) => label.id === labelId) !== undefined; + } + }); } return [id, section]; }); diff --git a/client/src/stores/toolStore.ts b/client/src/stores/toolStore.ts index 43dac1f78be0..ae799f972569 100644 --- a/client/src/stores/toolStore.ts +++ b/client/src/stores/toolStore.ts @@ -36,7 +36,18 @@ export interface ToolSection { version?: string; description?: string; links?: Record; - tools: string[]; + tools?: string[]; + elems?: ToolSection[]; + panel_labels?: string[]; +} + +export interface ToolSectionLabel { + model_class: string; + id: string; + text: string; + version?: string; + description?: string | null; + links?: Record | null; } // TODO: Use this in ToolSearch.vue @@ -147,8 +158,8 @@ export const useToolStore = defineStore("toolStore", () => { await axios .get(`${getAppRoot()}api/tools?in_panel=False`) .then(({ data }) => { - loading.value = false; saveAllTools(data.tools); + loading.value = false; }) .catch((error) => { console.error(error); diff --git a/lib/galaxy/tool_util/toolbox/base.py b/lib/galaxy/tool_util/toolbox/base.py index b28cbac33558..2a6b28aed697 100644 --- a/lib/galaxy/tool_util/toolbox/base.py +++ b/lib/galaxy/tool_util/toolbox/base.py @@ -1326,7 +1326,7 @@ def to_panel_view(self, trans, in_panel=True, tool_help=False, view=None, **kwds view = self._default_panel_view view_contents = {} rval[view] = view_contents - panel_elts = list(self.tool_panel_contents(trans, view=view, **kwds)) + panel_elts = self.tool_panel_contents(trans, view=view, **kwds) for elt in panel_elts: # Only use cache for objects that are Tools. if hasattr(elt, "tool_type"): diff --git a/lib/galaxy/tool_util/toolbox/panel.py b/lib/galaxy/tool_util/toolbox/panel.py index 8eb65a9d660c..5d49f0aea76f 100644 --- a/lib/galaxy/tool_util/toolbox/panel.py +++ b/lib/galaxy/tool_util/toolbox/panel.py @@ -74,10 +74,18 @@ def copy(self): return copy def to_dict(self, trans, link_details=False, tool_help=False, toolbox=None, only_ids=False): - """Return a dict that includes section's attributes.""" + """Return a dict that includes section's attributes. + + if `only_ids` is `True`, we store only the ids of the section's tools in `section.tools` + (and full `ToolSectionLabel` objects in `section.panel_labels` if any are present) + + if `only_ids` is `False`, we store the section's full `Tool` (and any other) objects in + `section.elems` + """ section_dict = super().to_dict() section_elts = [] + section_panel_labels = [] kwargs = dict(trans=trans, link_details=link_details, tool_help=tool_help) for elt in self.elems.values(): if hasattr(elt, "tool_type") and toolbox: @@ -85,11 +93,18 @@ def to_dict(self, trans, link_details=False, tool_help=False, toolbox=None, only section_elts.append(elt.id) else: section_elts.append(toolbox.get_tool_to_dict(trans, elt, tool_help=tool_help)) - elif only_ids is False: - section_elts.append(elt.to_dict(**kwargs)) + else: + # if section has a ToolSectionLabel within it + if only_ids and elt.text: + section_panel_labels.append(elt.to_dict(**kwargs)) + section_elts.append("panel_label_" + str(elt.id)) + else: + section_elts.append(elt.to_dict(**kwargs)) if only_ids: section_dict["tools"] = section_elts + if section_panel_labels: + section_dict["panel_labels"] = section_panel_labels else: section_dict["elems"] = section_elts @@ -98,6 +113,7 @@ def to_dict(self, trans, link_details=False, tool_help=False, toolbox=None, only def panel_items(self): return self.elems + class ToolSectionLabel(Dictifiable): """ A label for a set of tools that can be displayed above groups of tools