convert utilities.js to .ts and fix jests

improve typing in several places
This commit is contained in:
Ahmed Awan
2023-10-13 19:31:07 -05:00
parent 8809476a17
commit f9a4071d09
13 changed files with 819 additions and 256 deletions
@@ -1,6 +1,7 @@
import "jest-location-mock";
import { mount } from "@vue/test-utils";
import { createPinia } from "pinia";
import { getLocalVue } from "tests/jest/helpers";
import VueRouter from "vue-router";
@@ -11,19 +12,22 @@ localVue.use(VueRouter);
const router = new VueRouter();
describe("ToolSearch", () => {
it("test tools advanced filter panel", async () => {
it("test tools advanced filter panel navigation", async () => {
const pinia = createPinia();
const wrapper = mount(ToolSearch, {
propsData: {
currentPanelView: "default",
enableAdvanced: false,
showAdvanced: false,
toolbox: [],
toolsList: [],
currentPanel: {},
},
localVue,
router,
stubs: {
icon: { template: "<div></div>" },
},
pinia,
});
const $router = wrapper.vm.$router;
@@ -33,13 +37,6 @@ describe("ToolSearch", () => {
expect(wrapper.find("[data-description='wide toggle advanced search']").exists()).toBe(true);
expect(wrapper.find("[data-description='advanced filters']").exists()).toBe(true);
// Test: changing panel view should change search by section field to search by ontology
expect(wrapper.find("[placeholder='any section']").exists()).toBe(true);
await wrapper.setProps({ currentPanelView: "ontology:edam_operations" });
expect(wrapper.find("[placeholder='any section']").exists()).toBe(false);
expect(wrapper.find("[placeholder='any ontology']").exists()).toBe(true);
await wrapper.setProps({ currentPanelView: "default" });
// Test: keyup.esc (should toggle the view out) --- doesn't work from name (DelayedInput) field
const sectionField = wrapper.find("[placeholder='any section']");
expect(wrapper.emitted()["update:show-advanced"]).toBeUndefined();
@@ -52,6 +49,7 @@ describe("ToolSearch", () => {
const filterInputs = {
"[placeholder='any name']": "name-filter",
"[placeholder='any section']": "section-filter",
"[placeholder='any ontology']": "ontology-filter",
"[placeholder='any id']": "id-filter",
"[placeholder='any repository owner']": "owner-filter",
"[placeholder='any help text']": "help-filter",
@@ -72,6 +70,7 @@ describe("ToolSearch", () => {
const filterSettings = {
name: "name-filter",
section: "section-filter",
ontology: "ontology-filter",
id: "id-filter",
owner: "owner-filter",
help: "help-filter",
@@ -7,12 +7,16 @@ import { type Tool, type ToolSection, useToolStore } from "@/stores/toolStore";
import Filtering, { contains, type ValidFilter } from "@/utils/filtering";
import _l from "@/utils/localization";
import { type ToolSearchKeys } from "../utilities";
import DelayedInput from "@/components/Common/DelayedInput.vue";
import FilterMenu from "@/components/Common/FilterMenu.vue";
const router = useRouter();
const KEYS = { exact: 4, startsWith: 3, name: 2, description: 1, combined: 0 };
// Note: These are ordered by result priority (exact matches very top; words matches very bottom)
const KEYS: ToolSearchKeys = { exact: 5, startsWith: 4, name: 3, description: 2, combined: 1, wordMatch: 0 };
const FAVORITES = ["#favs", "#favorites", "#favourites"];
const MIN_QUERY_LENGTH = 3;
@@ -1,5 +1,6 @@
import { mount } from "@vue/test-utils";
import { useConfig } from "composables/config";
import { createPinia } from "pinia";
import { getLocalVue } from "tests/jest/helpers";
import ToolSection from "./ToolSection";
@@ -13,6 +14,11 @@ useConfig.mockReturnValue({
});
const localVue = getLocalVue();
const pinia = createPinia();
function sectionIsOpened(wrapper) {
return wrapper.find("[data-description='opened tool panel section']").exists();
}
describe("ToolSection", () => {
test("test tool section", () => {
@@ -23,6 +29,7 @@ describe("ToolSection", () => {
},
},
localVue,
pinia,
});
const nameElement = wrapper.findAll(".name");
expect(nameElement.at(0).text()).toBe("name");
@@ -46,8 +53,9 @@ describe("ToolSection", () => {
},
},
localVue,
pinia,
});
expect(wrapper.vm.opened).toBe(false);
expect(sectionIsOpened(wrapper)).toBe(false);
const $sectionName = wrapper.find(".name");
expect($sectionName.text()).toBe("tool_section");
await $sectionName.trigger("click");
@@ -76,22 +84,23 @@ describe("ToolSection", () => {
queryFilter: "test",
},
localVue,
pinia,
});
expect(wrapper.vm.opened).toBe(true);
expect(sectionIsOpened(wrapper)).toBe(true);
const $sectionName = wrapper.find(".name");
await $sectionName.trigger("click");
expect(wrapper.vm.opened).toBe(false);
expect(sectionIsOpened(wrapper)).toBe(false);
await wrapper.setProps({ queryFilter: "" });
expect(wrapper.vm.opened).toBe(false);
expect(sectionIsOpened(wrapper)).toBe(false);
await wrapper.setProps({ queryFilter: "test" });
expect(wrapper.vm.opened).toBe(true);
expect(sectionIsOpened(wrapper)).toBe(true);
await wrapper.setProps({ disableFilter: true });
expect(wrapper.vm.opened).toBe(true);
expect(sectionIsOpened(wrapper)).toBe(true);
await wrapper.setProps({ queryFilter: "" });
expect(wrapper.vm.opened).toBe(false);
expect(sectionIsOpened(wrapper)).toBe(false);
await $sectionName.trigger("click");
expect(wrapper.vm.opened).toBe(true);
expect(sectionIsOpened(wrapper)).toBe(true);
await wrapper.setProps({ queryFilter: "test" });
expect(wrapper.vm.opened).toBe(false);
expect(sectionIsOpened(wrapper)).toBe(false);
});
});
@@ -3,9 +3,8 @@ 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, type ToolSectionLabel as LabelType } from "@/stores/toolStore";
import { ToolSectionLabel } from "@/stores/toolStore";
import { useToolStore } from "@/stores/toolStore";
import { type Workflow } from "@/stores/workflowStore";
import ariaAlert from "@/utils/ariaAlert";
import Tool from "./Tool.vue";
@@ -13,8 +12,8 @@ import ToolPanelLabel from "./ToolPanelLabel.vue";
import ToolPanelLinks from "./ToolPanelLinks.vue";
const emit = defineEmits<{
(e: "onClick", tool: ToolType | Workflow, evt: Event): void;
(e: "onOperation", tool: ToolType | Workflow, evt: Event): void;
(e: "onClick", tool: any, evt: Event): void;
(e: "onOperation", tool: any, evt: Event): void;
}>();
const props = defineProps({
@@ -70,7 +69,7 @@ const elems = computed(() => {
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);
return props.category.panel_labels.find((label: ToolSectionLabel) => label.id === labelId);
} else {
return tool;
}
@@ -97,7 +96,7 @@ const sortedElements = computed(() => {
isConfigLoaded.value &&
config.value.toolbox_auto_sort === true &&
props.sortItems === true &&
!elems.value.some((el: ToolType) => el.text !== undefined && el.text !== "")
!elems.value.some((el: ToolSectionLabel) => el.text !== undefined && el.text !== "")
) {
const elements = [...elems.value];
const sorted = elements.sort((a, b) => {
@@ -150,10 +149,10 @@ function openToolSection(sectionId: string) {
function checkFilter() {
return !props.disableFilter && !!props.queryFilter;
}
function onClick(tool: ToolType | Workflow, evt: Event) {
function onClick(tool: any, evt: Event) {
emit("onClick", tool, evt);
}
function onOperation(tool: ToolType | Workflow, evt: Event) {
function onOperation(tool: any, evt: Event) {
emit("onOperation", tool, evt);
}
function toggleMenu(nextState = !opened.value) {
@@ -175,7 +174,7 @@ function toggleMenu(nextState = !opened.value) {
</a>
</div>
<transition name="slide">
<div v-if="opened">
<div v-if="opened" data-description="opened tool panel section">
<template v-for="[key, el] in sortedElements">
<ToolPanelLabel
v-if="category.text || el.model_class === 'ToolSectionLabel'"
+17 -7
View File
@@ -1,9 +1,10 @@
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 { useConfig } from "composables/config";
import toolsList from "./testToolsList";
import { filterTools, filterToolSections } from "./utilities";
import { createSortedResultObject, filterTools } from "./utilities";
jest.mock("composables/config");
useConfig.mockReturnValue({
@@ -14,8 +15,9 @@ useConfig.mockReturnValue({
});
describe("ToolBox", () => {
const toolsMock = toolsList;
const resultsMock = ["join1", "join_collections", "find1"];
const toolsMock = toolsList.tools;
const toolPanelMock = toolsListInPanel.default;
const resultsMock = ["liftOver1", "__FILTER_EMPTY_DATASETS__", "__UNZIP_COLLECTION__"];
let axiosMock;
beforeEach(async () => {
@@ -25,12 +27,20 @@ describe("ToolBox", () => {
it("test filter functions correctly matching: (1) Tools store array-of-objects with (2) Results array", async () => {
axiosMock
.onGet(`/api/tools`)
.replyOnce(200, toolsListInPanel)
.onGet(`/api/tools?in_panel=False`)
.replyOnce(200, toolsMock)
.onGet(/api\/tools?.*/)
.replyOnce(200, resultsMock);
const toolsResults = filterTools(toolsMock, resultsMock);
const toolsResultsSection = filterToolSections(toolsMock, resultsMock);
expect(toolsResults.length).toBe(3);
expect(toolsResultsSection.length).toBe(2);
const resultIds = Object.keys(toolsResults);
expect(resultIds.length).toBe(3);
const matchedTools = resultIds.map((id) => {
return { id: id, sections: [], order: 0 };
});
const toolsResultsSection = createSortedResultObject(matchedTools, toolPanelMock);
expect(toolsResultsSection.idResults).toEqual(resultIds);
const resultSectionIds = Object.keys(toolsResultsSection.resultPanel);
expect(resultSectionIds.length).toBe(2);
});
});
+56 -92
View File
@@ -3,24 +3,24 @@ import { library } from "@fortawesome/fontawesome-svg-core";
import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { storeToRefs } from "pinia";
import { computed, type PropType, type Ref, ref } from "vue";
import { computed, ComputedRef, type PropType, type Ref, ref } from "vue";
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, type ToolSectionLabel } from "@/stores/toolStore";
import { type Tool, type ToolSection as ToolSectionType } from "@/stores/toolStore";
import { useToolStore } from "@/stores/toolStore";
import { Workflow, type Workflow as WorkflowType } from "@/stores/workflowStore";
import localize from "@/utils/localization";
import { filterTools, isToolObject } from "./utilities.js";
import { filterTools, getValidPanelItems, getValidToolsInCurrentView, getValidToolsInEachSection } from "./utilities";
import ToolSearch from "./Common/ToolSearch.vue";
import ToolSection from "./Common/ToolSection.vue";
import UploadButton from "@/components/Upload/UploadButton.vue";
const SECTION_IDS_TO_EXCLUDE = ["expression_tools"];
const SECTION_IDS_TO_EXCLUDE = ["expression_tools"]; // if this isn't the Workflow Editor panel
const { openGlobalUploadModal } = useGlobalUploadModal();
const router = useRouter();
@@ -41,7 +41,7 @@ const props = defineProps({
panelQuery: { type: String, required: true },
editorWorkflows: { type: Array, default: null },
dataManagers: { type: Array, default: null },
moduleSections: { type: Array as PropType<Tool[]>, default: null },
moduleSections: { type: Array as PropType<Record<string, any>>, default: null },
});
library.add(faEye, faEyeSlash);
@@ -92,25 +92,12 @@ const dataManagerSection = computed(() => {
/** `toolsById` from `toolStore`, except it only has valid tools for `props.workflow` value */
const localToolsById = computed(() => {
const addedToolTexts: string[] = [];
if (toolStore.toolsById && Object.keys(toolStore.toolsById).length > 0) {
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 getValidToolsInCurrentView(
toolStore.toolsById,
props.workflow,
!props.workflow ? SECTION_IDS_TO_EXCLUDE : []
);
}
return {};
});
@@ -118,37 +105,16 @@ const localToolsById = computed(() => {
/** `currentPanel` from `toolStore`, except it only has valid tools and sections for `props.workflow` value */
const localSectionsById = computed(() => {
const validToolIdsInCurrentView = Object.keys(localToolsById.value);
// 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((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];
});
const updatedSectionEntries = sectionEntries.filter(([id, section]) => {
if (isToolObject(section) && validToolIdsInCurrentView.includes(id)) {
// is a `Tool` and is in `localToolsById`
return true;
} else if (section.tools === undefined) {
// is neither a `Tool` nor a `ToolSection`, maybe a `ToolSectionLabel`
return true;
} else if (section.tools.length > 0 && (props.workflow || !SECTION_IDS_TO_EXCLUDE.includes(id))) {
// is a `ToolSection` with tools; check workflow compatibility (based on props.workflow)
return true;
} else {
return false;
}
});
return Object.fromEntries(updatedSectionEntries);
// Looking within each `ToolSection`, and filtering on child elements
const sectionEntries = getValidToolsInEachSection(validToolIdsInCurrentView, currentPanel.value);
// Looking at each item in the panel now (not within each child)
return getValidPanelItems(
sectionEntries,
validToolIdsInCurrentView,
!props.workflow ? SECTION_IDS_TO_EXCLUDE : []
) as Record<string, Tool | ToolSectionType>;
});
const toolsList = computed(() => Object.values(localToolsById.value));
@@ -159,73 +125,69 @@ const toolsList = computed(() => Object.values(localToolsById.value));
* If we have results for search, we show tools in sections or just tools,
* based on whether `showSections` is true or false
*/
const localPanel = computed(() => {
const localPanel: ComputedRef<Record<string, Tool | ToolSectionType> | null> = computed(() => {
if (hasResults.value) {
if (showSections.value) {
return resultPanel.value;
} else {
return filterTools(localToolsById.value, results.value);
return filterTools(localToolsById.value, results.value) as Record<string, Tool | ToolSectionType>;
}
} else {
return localSectionsById.value;
}
});
const sectionIds = computed(() => Object.keys(localPanel.value));
const sectionIds = computed(() => Object.keys(localPanel.value || {}));
const workflows = computed(() => {
if (props.workflow && props.editorWorkflows) {
return props.editorWorkflows;
} else {
const Galaxy = getGalaxyInstance();
const storedWorkflowMenuEntries = Galaxy && Galaxy.config.stored_workflow_menu_entries;
if (storedWorkflowMenuEntries) {
const returnedWfs = [];
if (!props.workflow) {
returnedWfs.push({
title: localize("All workflows"),
href: `${getAppRoot()}workflows/list`,
id: "list",
});
}
const storedWfs = [
...storedWorkflowMenuEntries.map((menuEntry: Workflow) => {
return {
id: menuEntry.id,
title: menuEntry.name,
href: `${getAppRoot()}workflows/run?id=${menuEntry.id}`,
};
}),
];
return returnedWfs.concat(storedWfs);
} else {
return [];
const Galaxy = getGalaxyInstance();
const storedWorkflowMenuEntries = Galaxy && Galaxy.config.stored_workflow_menu_entries;
if (storedWorkflowMenuEntries) {
const returnedWfs = [];
if (!props.workflow) {
returnedWfs.push({
title: localize("All workflows") as string,
href: `${getAppRoot()}workflows/list`,
id: "list",
});
}
const storedWfs = [
...storedWorkflowMenuEntries.map((menuEntry: Workflow) => {
return {
id: menuEntry.id,
title: menuEntry.name,
href: `${getAppRoot()}workflows/run?id=${menuEntry.id}`,
};
}),
];
return returnedWfs.concat(storedWfs);
} else {
return [];
}
});
const workflowSection = computed(() => {
return {
name: localize("Workflows"),
elems: workflows.value,
elems: props.workflow && props.editorWorkflows,
};
});
function onInsertModule(module: Tool, event: Event) {
function onInsertModule(module: Record<string, any>, event: Event) {
event.preventDefault();
emit("onInsertModule", module.name, module.title);
}
function onInsertWorkflow(workflow: WorkflowType | Tool, event: Event) {
function onInsertWorkflow(workflow: WorkflowType, event: Event) {
event.preventDefault();
emit("onInsertWorkflow", workflow.latest_id, workflow.name);
}
function onInsertWorkflowSteps(workflow: WorkflowType | Tool) {
function onInsertWorkflowSteps(workflow: WorkflowType) {
emit("onInsertWorkflowSteps", workflow.id, workflow.step_count);
}
function onToolClick(tool: Tool | WorkflowType, evt: Event) {
function onToolClick(tool: Tool, evt: Event) {
if (!props.workflow) {
if (tool.id === "upload1") {
evt.preventDefault();
@@ -324,21 +286,21 @@ function setButtonText() {
:category="category"
tool-key="name"
:section-name="category.name"
:query-filter="queryFilter"
:query-filter="queryFilter || undefined"
:disable-filter="true"
@onClick="onInsertModule" />
</div>
<ToolSection
v-if="hasDataManagerSection"
:category="dataManagerSection"
:query-filter="queryFilter"
:query-filter="queryFilter || undefined"
:disable-filter="true"
@onClick="onToolClick" />
<div v-for="(sectionId, key) in sectionIds" :key="key">
<ToolSection
v-if="localPanel[sectionId]"
:category="localPanel[sectionId] || {}"
:query-filter="queryFilter"
:query-filter="queryFilter || undefined"
@onClick="onToolClick" />
</div>
</div>
@@ -351,7 +313,7 @@ function setButtonText() {
:sort-items="false"
operation-icon="fa fa-files-o"
operation-title="Insert individual steps."
:query-filter="queryFilter"
:query-filter="queryFilter || undefined"
:disable-filter="true"
@onClick="onInsertWorkflow"
@onOperation="onInsertWorkflowSteps" />
@@ -360,7 +322,9 @@ function setButtonText() {
<div id="internal-workflows" class="toolSectionBody">
<div class="toolSectionBg" />
<div v-for="wf in workflows" :key="wf.id" class="toolTitle">
<a class="title-link" :href="wf.href">{{ wf.title }}</a>
<a class="title-link" href="javascript:void(0)" @click="router.push(wf.href)">{{
wf.title
}}</a>
</div>
</div>
</div>
@@ -1,72 +0,0 @@
[
{
"model_class": "ToolSection",
"id": "group_1",
"name": "Group 1",
"elems": [
{
"model_class": "Tool",
"name": "Join",
"id": "join1",
"description": "two files",
"panel_section_id": "group_1",
"panel_section_name": "Group 1"
},
{
"model_class": "Tool",
"name": "Remove",
"id": "remove1",
"description": "lines",
"panel_section_id": "group_1",
"panel_section_name": "Group 1"
},
{
"model_class": "Tool",
"name": "Cut",
"id": "cut1",
"description": "lines",
"panel_section_id": "group_1",
"panel_section_name": "Group 1"
},
{
"model_class": "Tool",
"name": "Find",
"id": "find1",
"description": "joined dataset",
"panel_section_id": "group_1",
"panel_section_name": "Group 1"
}
]
},
{
"model_class": "ToolSection",
"id": "group_2",
"name": "Group 2",
"elems": [
{
"model_class": "Tool",
"name": "Join Collections",
"id": "join_collections",
"description": "",
"panel_section_id": "group_2",
"panel_section_name": "Group 2"
},
{
"model_class": "Tool",
"name": "Create Collections",
"id": "create_collections",
"description": "from datasets",
"panel_section_id": "group_2",
"panel_section_name": "Group 2"
}
]
},
{
"model_class": "Tool",
"name": "Remove duplicate",
"id": "remove_duplicate",
"description": "lines from dataset",
"panel_section_id": "remove_duplicate",
"panel_section_name": "Remove duplicate"
}
]
+81 -41
View File
@@ -1,11 +1,11 @@
import toolsList from "components/ToolsView/testData/toolsList";
import toolsListInPanel from "components/ToolsView/testData/toolsListInPanel";
import {
createSortedResultObject,
createWhooshQuery,
determineWidth,
filterTools,
filterToolSections,
flattenTools,
searchToolsByKeys,
} from "./utilities";
@@ -18,27 +18,35 @@ describe("test helpers in tool searching utilities and panel handling", () => {
});
});
const tempToolsList = [
{
elems: [
{
panel_section_name: "FASTA/FASTQ",
description: "Extract UMI from fastq files",
id: "toolshed.g2.bx.psu.edu/repos/iuc/umi_tools_extract/umi_tools_extract/1.1.2+galaxy2",
name: "UMI-tools extract",
},
{
panel_section_name: "FASTA/FASTQ",
description: "Extract UMI from (fasta files)",
id: "umi_tools_reduplicate",
name: "UMI-tools reduplicate",
},
],
model_class: "ToolSection",
id: "fasta/fastq",
name: "FASTA/FASTQ",
const tempToolPanel = {
default: {
"fasta/fastq": {
tools: [
"toolshed.g2.bx.psu.edu/repos/iuc/umi_tools_extract/umi_tools_extract/1.1.2+galaxy2",
"umi_tools_reduplicate",
],
model_class: "ToolSection",
id: "fasta/fastq",
name: "FASTA/FASTQ",
},
},
];
};
const tempToolsList = {
tools: {
"toolshed.g2.bx.psu.edu/repos/iuc/umi_tools_extract/umi_tools_extract/1.1.2+galaxy2": {
panel_section_name: "FASTA/FASTQ",
description: "Extract UMI from fastq files",
id: "toolshed.g2.bx.psu.edu/repos/iuc/umi_tools_extract/umi_tools_extract/1.1.2+galaxy2",
name: "UMI-tools extract",
},
umi_tools_reduplicate: {
panel_section_name: "FASTA/FASTQ",
description: "Extract UMI from (fasta files)",
id: "umi_tools_reduplicate",
name: "UMI-tools reduplicate",
},
},
};
describe("test helpers in tool searching utilities", () => {
it("test parsing helper that converts settings to whoosh query", async () => {
@@ -72,7 +80,8 @@ describe("test helpers in tool searching utilities", () => {
"__ZIP_COLLECTION__",
],
keys: { description: 1, name: 0 },
list: toolsList,
tools: Object.values(toolsList.tools),
panel: toolsListInPanel.default,
},
{
// name prioritized
@@ -84,42 +93,48 @@ describe("test helpers in tool searching utilities", () => {
"__FILTER_EMPTY_DATASETS__",
],
keys: { description: 0, name: 1 },
list: toolsList,
tools: Object.values(toolsList.tools),
panel: toolsListInPanel.default,
},
{
// whitespace precedes to ensure query.trim() works
q: " filter empty datasets",
expectedResults: ["__FILTER_EMPTY_DATASETS__"],
keys: { description: 1, name: 2, combined: 0 },
list: toolsList,
tools: Object.values(toolsList.tools),
panel: toolsListInPanel.default,
},
{
// hyphenated tool-name is searchable
q: "uMi tools extract ",
expectedResults: ["toolshed.g2.bx.psu.edu/repos/iuc/umi_tools_extract/umi_tools_extract/1.1.2+galaxy2"],
keys: { description: 1, name: 2 },
list: tempToolsList,
tools: Object.values(tempToolsList.tools),
panel: tempToolPanel.default,
},
{
// parenthesis is searchable
q: "from FASTA files",
// parenthesis (and other chars) are searchable
q: "from FASTA:files",
expectedResults: ["umi_tools_reduplicate"],
keys: { description: 1, name: 2 },
list: tempToolsList,
tools: Object.values(tempToolsList.tools),
panel: tempToolPanel.default,
},
{
// id is not searchable if not identified by colon
q: "__ZIP_COLLECTION__",
expectedResults: [],
keys: { description: 1, name: 2 },
list: toolsList,
tools: Object.values(toolsList.tools),
panel: toolsListInPanel.default,
},
{
// id is searchable if provided "id:"
q: "id:__ZIP_COLLECTION__",
expectedResults: ["__ZIP_COLLECTION__"],
keys: { description: 1, name: 2 },
list: toolsList,
tools: Object.values(toolsList.tools),
panel: toolsListInPanel.default,
},
{
// id is searchable if provided "tool_id:"
@@ -129,11 +144,20 @@ describe("test helpers in tool searching utilities", () => {
"umi_tools_reduplicate",
],
keys: { description: 1, name: 2 },
list: tempToolsList,
tools: Object.values(tempToolsList.tools),
panel: tempToolPanel.default,
},
// if at least couple words match, return results
{
q: "filter datasets",
expectedResults: ["__FILTER_FAILED_DATASETS__", "__FILTER_EMPTY_DATASETS__"],
keys: { combined: 1, wordMatch: 0 },
tools: Object.values(toolsList.tools),
panel: toolsListInPanel.default,
},
];
searches.forEach((search) => {
const { results } = searchToolsByKeys(flattenTools(search.list), search.keys, search.q);
const { results } = searchToolsByKeys(search.tools, search.keys, search.q, "default", search.panel);
expect(results).toEqual(search.expectedResults);
});
});
@@ -143,13 +167,25 @@ describe("test helpers in tool searching utilities", () => {
const keys = { description: 1, name: 2, combined: 0 };
const filterQueries = ["Fillter", "FILYER", " Fitler", " filtr"];
filterQueries.forEach((q) => {
const { results, closestTerm } = searchToolsByKeys(flattenTools(toolsList), keys, q);
const { results, closestTerm } = searchToolsByKeys(
Object.values(toolsList.tools),
keys,
q,
"default",
toolsListInPanel.default
);
expect(results).toEqual(expectedResults);
expect(closestTerm).toEqual("filter");
});
const queries = ["datases from a collection", "from a colleection"];
queries.forEach((q) => {
const { results } = searchToolsByKeys(flattenTools(toolsList), keys, q);
const { results } = searchToolsByKeys(
Object.values(toolsList.tools),
keys,
q,
"default",
toolsListInPanel.default
);
expect(results).toEqual(expectedResults);
});
});
@@ -157,12 +193,16 @@ describe("test helpers in tool searching utilities", () => {
it("test tool filtering helpers on toolsList given list of ids", async () => {
const ids = ["__FILTER_FAILED_DATASETS__", "liftOver1"];
// check length of first section from imported const toolsList
expect(toolsList.find((section) => section.id == "collection_operations").elems).toHaveLength(4);
expect(toolsListInPanel.default["collection_operations"].tools).toHaveLength(4);
// check length of same section from filtered toolsList
expect(
filterToolSections(toolsList, ids).find((section) => section.id == "collection_operations").elems
).toHaveLength(1);
// check length of filtered tools without sections
expect(filterTools(toolsList, ids)).toHaveLength(2);
const matchedTools = ids.map((id) => {
return { id: id, sections: [], order: 0 };
});
const toolResultsPanel = createSortedResultObject(matchedTools, toolsListInPanel.default);
const toolResultsSection = toolResultsPanel.resultPanel["collection_operations"];
expect(toolResultsSection.tools).toHaveLength(1);
// check length of filtered tools (regardless of sections)
const filteredToolIds = Object.keys(filterTools(toolsList.tools, ids));
expect(filteredToolIds).toHaveLength(2);
});
});
+530
View File
@@ -0,0 +1,530 @@
/**
* Utilities file for Panel Searches (panel/client search + advanced/backend search)
* Note: Any mention of "DL" in this file refers to the Demerau-Levenshtein distance algorithm
*/
import { orderBy } from "lodash";
import {
type FilterSettings as ToolFilters,
type Tool,
type ToolSection,
type ToolSectionLabel,
} from "@/stores/toolStore";
import levenshteinDistance from "@/utils/levenshtein";
const TOOL_ID_KEYS = ["id", "tool_id"];
const STRING_REPLACEMENTS: string[] = [" ", "-", "\\(", "\\)", "'", ":", `"`];
const MINIMUM_DL_LENGTH = 5; // for Demerau-Levenshtein distance
const MINIMUM_WORD_MATCH = 2; // for word match
const UNSECTIONED_SECTION = {
// to return a section for unsectioned tools
model_class: "ToolSection",
id: "unsectioned",
name: "Unsectioned Tools",
description: "Tools that don't appear under any section in the unsearched panel",
};
const VALID_SECTION_TYPES = ["edam_operations", "edam_topics"]; // other than `default`
export interface ToolSearchKeys {
[key: string | keyof Tool]: number | undefined;
exact?: number;
startsWith?: number;
name?: number;
description?: number;
combined?: number;
wordMatch?: number;
}
interface SearchMatch {
id: string;
sections: (string | undefined)[];
order: number;
}
// Converts filterSettings { key: value } to query = "key:value"
export function createWorkflowQuery(filterSettings: Record<string, string | boolean>) {
let query = "";
query = Object.entries(filterSettings)
.filter(([, value]) => value)
.map(([filter, value]) => {
if (value === true) {
return `is:${filter}`;
}
return `${filter}:${value}`;
})
.join(" ");
if (Object.keys(filterSettings).length == 1 && filterSettings.name) {
return filterSettings.name as string;
}
return query;
}
/** Converts filters into tool search backend whoosh query.
* @param filterSettings e.g.: {"name": "Tool Name", "section": "Collection", ...}
* @returns parsed Whoosh `query`
* @example
* createWhooshQuery(filterSettings = {"name": "skew", "ontology": "topic_0797"})
* return query = "(name:(skew) name_exact:(skew) description:(skew)) AND (edam_topics:(topic_0797) AND )"
*/
export function createWhooshQuery(filterSettings: ToolFilters) {
let query = "(";
// add description+name_exact fields = name, to do a combined OrGroup at backend
const name = filterSettings["name"];
if (name) {
query += "name:(" + name + ") ";
query += "name_exact:(" + name + ") ";
query += "description:(" + name + ")";
}
query += ") AND (";
for (const [key, filterValue] of Object.entries(filterSettings)) {
if (key === "ontology" && filterValue.includes("operation")) {
query += "edam_operations:(" + filterValue + ") AND ";
} else if (key === "ontology" && filterValue.includes("topic")) {
query += "edam_topics:(" + filterValue + ") AND ";
} else if (key == "id") {
query += "id_exact:(" + filterValue + ") AND ";
} else if (key != "name") {
query += key + ":(" + filterValue + ") AND ";
}
}
query += ")";
return query;
}
// Determines width given the root and draggable element, smallest and largest size and the current position
export function determineWidth(
rectRoot: { left: number; right: number },
rectDraggable: { left: number },
minWidth: number,
maxWidth: number,
direction: string,
positionX: number
) {
let newWidth = null;
if (direction === "right") {
const offset = rectRoot.left - rectDraggable.left;
newWidth = rectRoot.right - positionX - offset;
} else {
const offset = rectRoot.right - rectDraggable.left;
newWidth = positionX - rectRoot.left + offset;
}
return Math.max(minWidth, Math.min(maxWidth, newWidth));
}
/**
* @param toolsById - all tools, keyed by id
* @param results - list of result tool ids
* @returns filtered tool results (by id)
*/
export function filterTools(toolsById: Record<string, Tool>, results: string[]) {
const filteredTools: Record<string, Tool> = {};
for (const id of results) {
const localTool = toolsById[id];
if (localTool !== undefined) {
filteredTools[id] = localTool;
}
}
return filteredTools;
}
/** Returns a `toolsById` object containing tools that meet required conditions
* based on params.
* @param toolsById - object of tools, keyed by id
* @param isWorkflowPanel - whether or not the ToolPanel is in Workflow Editor
* @param excludedSectionIds - ids for sections whose tools will be excluded
*/
export function getValidToolsInCurrentView(
toolsById: Record<string, Tool>,
isWorkflowPanel = false,
excludedSectionIds: string[] = []
) {
const addedToolTexts: string[] = [];
const toolEntries = Object.entries(toolsById).filter(([, tool]) => {
// filter out duplicate tools (different ids, same exact name+description)
// related ticket: https://github.com/galaxyproject/galaxy/issues/16145
const toolText = 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 &&
!(isWorkflowPanel && !tool.is_workflow_compatible) &&
!excludedSectionIds.includes(tool.panel_section_id || "")
);
});
return Object.fromEntries(toolEntries);
}
/** Looks in each section of `currentPanel` and filters `section.tools` on `validToolIdsInCurrentView` */
export function getValidToolsInEachSection(
validToolIdsInCurrentView: string[],
currentPanel: Record<string, Tool | ToolSection>
) {
return Object.entries(currentPanel).map(([id, section]) => {
const validatedSection = { ...section } as ToolSection;
if (validatedSection.tools && Array.isArray(validatedSection.tools)) {
// filter on valid tools and panel_labels in this section
validatedSection.tools = validatedSection.tools.filter((toolId) => {
if (validToolIdsInCurrentView.includes(toolId)) {
return true;
} else if (toolId.startsWith("panel_label_") && validatedSection.panel_labels) {
// panel_label_ is a special id where there is a label within a section
const labelId = toolId.split("panel_label_")[1];
return validatedSection.panel_labels.find((label) => label.id === labelId) !== undefined;
}
});
}
return [id, validatedSection];
});
}
/**
* @param items - `[id, PanelItem]` entries (from the `currentPanel` object)
* @param validToolIdsInCurrentView - tool ids that are valid in current view
* @param excludedSectionIds - any section ids to exclude
* @returns a `currentPanel` object containing sections/tools/labels that meet required conditions
*/
export function getValidPanelItems(
items: (string | ToolSection)[][],
validToolIdsInCurrentView: string[],
excludedSectionIds: string[] = []
) {
const validEntries = items.filter(([id, item]) => {
id = id as string;
item = item as Tool | ToolSection;
if (isToolObject(item as Tool) && validToolIdsInCurrentView.includes(id)) {
// is a `Tool` and is in `localToolsById`
return true;
} else if (item.tools === undefined) {
// is neither a `Tool` nor a `ToolSection`, maybe a `ToolSectionLabel`
return true;
} else if (item.tools && item.tools.length > 0 && !excludedSectionIds.includes(id)) {
// is a `ToolSection` with tools; is not an excluded section
return true;
} else {
return false;
}
});
return Object.fromEntries(validEntries);
}
/**
* Given toolbox, keys to sort/search results by and a search query,
* Does a direct string.match() comparison to find results,
* If that produces nothing, runs DL distance alg to allow misspells
*
* @param tools - toolbox
* @param keys - keys to sort and search results by
* @param query - a search query
* @param panelView - panel view, to find section_id for each tool
* @param currentPanel - current ToolPanel with { section_id: { tools: [tool ids] }, ... }
* @param usesDL - Optional: used for recursive call with DL if no string.match()
* @returns an object containing
* - results: array of tool ids that match the query
* - resultPanel: a ToolPanel with only the results for the current panelView
* - closestTerm: Optional: closest matching term for DL (in case no match with query)
*
* all sorted by order of keys that are being searched (+ closest matching term if DL)
*/
export function searchToolsByKeys(
tools: Tool[],
keys: ToolSearchKeys,
query: string,
panelView = "default",
currentPanel: Record<string, Tool | ToolSection>,
usesDL = false
): {
results: string[];
resultPanel: Record<string, Tool | ToolSection>;
closestTerm: string | null;
} {
const matchedTools: SearchMatch[] = [];
let closestTerm = null;
// if user's query = "id:1234" or "tool_id:1234", only search for id
const id = processForId(query, TOOL_ID_KEYS);
if (id) {
query = id;
keys = { id: 1 };
}
const queryWords = query.trim().toLowerCase().split(" ");
const queryValue = sanitizeString(query.trim().toLowerCase(), STRING_REPLACEMENTS);
for (const tool of tools) {
for (const key of Object.keys(keys)) {
if (tool[key as keyof Tool] || key === "combined") {
let actualValue = "";
// key = "combined" is a special case for searching name + description
if (key === "combined") {
actualValue = `${tool.name.trim()} ${tool.description.trim()}`.toLowerCase();
} else {
actualValue = (tool[key as keyof Tool] as string)?.trim().toLowerCase();
}
// get all (space separated) words in actualValue for tool (for DL)
const actualValueWords = actualValue.split(" ");
actualValue = sanitizeString(actualValue, STRING_REPLACEMENTS);
// do we care for exact matches && is it an exact match ?
let order =
keys.exact !== undefined && actualValue === queryValue
? (keys.exact as number)
: (keys[key] as number);
// do we care for startsWith && does it actualValue start with query ?
order =
keys.startsWith !== undefined &&
order !== keys.exact &&
key === "name" &&
actualValue.startsWith(queryValue)
? keys.startsWith
: order;
const wordMatches = actualValueWords.filter((word) => queryWords.includes(word));
if (!usesDL) {
if (actualValue.match(queryValue)) {
// if string.match() returns true, matching tool found
matchedTools.push({ id: tool.id, sections: getPanelSectionsForTool(tool, panelView), order });
break;
} else if (
key === "combined" &&
keys.wordMatch !== undefined &&
wordMatches.length >= MINIMUM_WORD_MATCH
) {
// we are looking at combined name+description, and there are enough word matches
matchedTools.push({
id: tool.id,
sections: getPanelSectionsForTool(tool, panelView),
order: keys.wordMatch,
});
break;
}
} else if (usesDL) {
// if string.match() returns false, try DL distance once to see if there is a closestSubstring
let substring = null;
if ((key == "name" || key == "description") && queryValue.length >= MINIMUM_DL_LENGTH) {
substring = closestSubstring(queryValue, actualValue);
}
// there is a closestSubstring: matching tool found
if (substring) {
// get the closest matching term for substring
const foundTerm = matchingTerm(actualValueWords, substring);
if (foundTerm && (!closestTerm || (closestTerm && foundTerm.length < closestTerm.length))) {
closestTerm = foundTerm;
}
matchedTools.push({
id: tool.id,
sections: getPanelSectionsForTool(tool, panelView),
order,
});
break;
}
}
}
}
}
// no results with string.match(): recursive call with usesDL
if (!id && !usesDL && matchedTools.length == 0) {
return searchToolsByKeys(tools, keys, query, panelView, currentPanel, true);
}
const { idResults, resultPanel } = createSortedResultObject(matchedTools, currentPanel);
return { results: idResults, resultPanel: resultPanel, closestTerm: closestTerm };
}
/**
* Orders the matchedTools by order of keys that are being searched, and creates a resultPanel
* @param matchedTools containing { id: tool id, sections: [section ids], order: order }
* @param currentPanel current ToolPanel for current view
* @returns an object containing
* - idResults: array of tool ids that match the query
* - resultPanel: a ToolPanel with only the results
*/
export function createSortedResultObject(
matchedTools: SearchMatch[],
currentPanel: Record<string, Tool | ToolSection>
) {
const idResults: string[] = [];
// creating a sectioned results object ({section_id: [tool ids], ...}), keeping
// track of the best version of each tool, and also sorting by indexed order of keys
const resultPanel = orderBy(matchedTools, ["order"], ["desc"]).reduce(
(acc: Record<string, Tool | ToolSection>, match: SearchMatch) => {
// we either found specific section(s) for tool, or we need to search all sections
const sections: (string | undefined)[] =
match.sections.length !== 0 ? match.sections : Object.keys(currentPanel);
for (const section of sections) {
let toolAdded = false;
const existingPanelItem = section ? currentPanel[section] : undefined;
if (existingPanelItem && section) {
if (
(existingPanelItem as ToolSection).tools &&
(existingPanelItem as ToolSection).tools?.includes(match.id)
) {
// it has tools so is a section, and it has the tool we're looking for
// if we haven't seen this section yet, create it in the resultPanel
let existingSection = acc[section] as ToolSection;
if (!existingSection) {
existingSection = { ...existingPanelItem };
existingSection.tools = [];
}
existingSection.tools?.push(match.id);
acc[section] = existingSection;
toolAdded = true;
} else if (isToolObject(existingPanelItem as Tool) && existingPanelItem.id === match.id) {
// it is a tool, and it is the tool we're looking for
// put in it the "Unsectioned Tools" section (if it doesn't exist, create it)
const unsectionedId = UNSECTIONED_SECTION.id;
let unsectionedSection = acc[unsectionedId] as ToolSection;
if (!unsectionedSection) {
unsectionedSection = { ...UNSECTIONED_SECTION };
unsectionedSection.tools = [];
}
unsectionedSection.tools?.push(match.id);
acc[unsectionedId] = unsectionedSection;
toolAdded = true;
}
if (toolAdded && !idResults.includes(match.id)) {
idResults.push(match.id);
}
}
}
return acc;
},
{}
);
return { idResults, resultPanel };
}
/**
* Gets the section(s) a tool belongs to for a given panelView.
* Unless view=`default`, all other views must be of format `class:view_type`,
* where `Tool` object has a `view_name` key containing an array of section ids.
* e.g.: view = `ontology:edam_operations` => `Tool.edam_operations = [section ids]`.
*
* Therefore, this would not handle the case where view = `ontology:edam_merged`, since
* Tool.edam_merged is not a valid key, and we would just return `[uncategorized]`.
*
* Just prevents looking through whole panel to find section id for tool,
* we still end up doing that (in `createSortedResultObject`) in case we return [] here
* @param tool
* @param panelView
* @returns Array of section ids
*/
function getPanelSectionsForTool(tool: Tool, panelView: string) {
if (panelView === "default" && tool.panel_section_id) {
if (tool.panel_section_id.startsWith("tool_")) {
return [tool.panel_section_id.split("tool_")[1]];
} else {
return [tool.panel_section_id];
}
} else if (panelView !== "default") {
const sectionType = panelView.split(":")[1] as keyof Tool;
if (
sectionType &&
VALID_SECTION_TYPES.includes(sectionType as string) &&
(tool[sectionType] as string[])?.length !== 0
) {
return tool[sectionType] as string[];
} else {
return ["uncategorized"];
}
}
return [];
}
/**
*
* @param query
* @param actualStr
* @returns substring with smallest DL distance, or null
*/
function closestSubstring(query: string, actualStr: string) {
// Max distance a query and substring can be apart
const maxDistance = 1;
// Create an array of all actualStr substrings that are query length, query length -1, and query length + 1
const substrings = Array.from({ length: actualStr.length - query.length + 1 }, (_, i) =>
actualStr.substr(i, query.length)
);
if (query.length > 1) {
substrings.push(
...Array.from({ length: actualStr.length - query.length + 2 }, (_, i) =>
actualStr.substr(i, query.length - 1)
)
);
}
if (actualStr.length > query.length) {
substrings.push(
...Array.from({ length: actualStr.length - query.length }, (_, i) => actualStr.substr(i, query.length + 1))
);
}
// check to see if any substrings have a levenshtein distance less than the max distance
for (const substring of substrings) {
if (levenshteinDistance(query, substring, true) <= maxDistance) {
return substring;
}
}
return null;
}
export function isToolObject(tool: Tool | ToolSection | ToolSectionLabel) {
// toolbox overhaul with typing will simplify this dramatically...
// Right now, our shorthand is that tools have no 'text', and don't match
// the model_class of the section/label.
if (
!(tool as ToolSectionLabel).text &&
tool.model_class !== "ToolSectionLabel" &&
tool.model_class !== "ToolSection" &&
(tool as ToolSection).tools === undefined
) {
return true;
}
return false;
}
// given array and a substring, get the closest matching term for substring
function matchingTerm(termArray: string[], substring: string) {
const sanitized = sanitizeString(substring);
for (const i in termArray) {
const term = termArray[i];
if (term?.match(sanitized)) {
return term;
}
}
return null;
}
/**
*
* @param value - to be sanitized
* @param targets - Optional: characters to replace
* @param substitute - Optional: replacement character
* @returns sanitized string
*/
function sanitizeString(value: string, targets: string[] = [], substitute = "") {
let sanitized = value;
targets.forEach((rep) => {
sanitized = sanitized.replace(new RegExp(rep, "g"), substitute);
});
return sanitized.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/**
* If the query is of the form "id:1234" (or "tool_id:1234"), return the id.
* Otherwise, return null.
* @param query - the raw query
* @param keys - Optional: keys to check for (default: ["id"])
* @returns id or null
*/
function processForId(query: string, keys = ["id"]) {
for (const key of keys) {
if (query.includes(`${key}:`)) {
return query.split(`${key}:`)[1]?.trim();
}
}
return null;
}
@@ -21,6 +21,7 @@ describe("InstallationSettings", () => {
changesetRevision: "changesetRevision",
requiresPanel: true,
toolshedUrl: "toolshedUrl",
currentPanel: {},
},
localVue,
});
@@ -0,0 +1,78 @@
<script setup lang="ts">
import { type Ref, ref } from "vue";
import { useRouter } from "vue-router/composables";
import { createWorkflowQuery } from "@/components/Panels/utilities";
import _l from "@/utils/localization";
const router = useRouter();
type FilterSettings = {
name?: string;
tag?: string;
published?: boolean;
shared_with_me?: boolean;
deleted?: boolean;
};
const options = [
{ text: "Yes", value: true },
{ text: "No", value: false },
];
const filterSettings: Ref<FilterSettings> = ref({
published: false,
shared_with_me: false,
deleted: false,
});
function onSearch() {
const query = createWorkflowQuery(filterSettings.value as Record<string, string | boolean>);
const path = "/workflows/list";
const routerParams = query ? { path, query: { query } } : { path };
router.push(routerParams);
}
</script>
<template>
<div>
<div description="advanced workflow filters" @keyup.enter="onSearch">
<small class="mt-1">Filter by name:</small>
<b-form-input v-model="filterSettings.name" size="sm" placeholder="any name" />
<small class="mt-1">Filter by tag:</small>
<b-form-input v-model="filterSettings.tag" size="sm" placeholder="any tag" />
<small>Search published workflows:</small>
<b-form-group class="m-0">
<b-form-radio-group
v-model="filterSettings.published"
:options="options"
size="sm"
buttons
description="filter published" />
</b-form-group>
<small>Search shared workflows:</small>
<b-form-group class="m-0">
<b-form-radio-group
v-model="filterSettings.shared_with_me"
:options="options"
size="sm"
buttons
description="filter shared" />
</b-form-group>
<small>Search deleted workflows:</small>
<b-form-group class="m-0">
<b-form-radio-group
v-model="filterSettings.deleted"
:options="options"
size="sm"
buttons
description="filter deleted" />
</b-form-group>
<div class="mt-3">
<b-button class="mr-1" size="sm" variant="primary" @click="onSearch">
<icon icon="search" />
<span>{{ _l("Search") }}</span>
</b-button>
</div>
</div>
</div>
</template>
+16 -15
View File
@@ -10,25 +10,28 @@ import { createWhooshQuery, filterTools } from "@/components/Panels/utilities";
import { useUserLocalStorage } from "@/composables/userLocalStorage";
import { getAppRoot } from "@/onload/loadConfig";
/* interfaces */
export interface Tool {
model_class: string;
id: string;
name: string;
version: string;
description: string;
hidden: boolean;
labels: string[];
edam_operations: string[];
edam_topics: string[];
hidden: "" | boolean;
is_workflow_compatible: boolean;
panel_section_name: string;
panel_section_id: string;
xrefs: string[];
config_file: string;
link: string;
min_width: number;
target: string;
panel_section_id: string | null;
panel_section_name: string | null;
form_style: string;
text?: string;
current_section_ids?: string[]; // for purely setting it = currentPanelView section id
title?: string;
latest_id?: string;
step_count?: number;
disabled?: boolean;
}
/* interfaces */
export interface ToolSection {
model_class: string;
id: string;
@@ -37,8 +40,8 @@ export interface ToolSection {
description?: string;
links?: Record<string, string>;
tools?: string[];
elems?: ToolSection[];
panel_labels?: string[];
elems?: (Tool | ToolSection)[];
panel_labels?: any[];
}
export interface ToolSectionLabel {
@@ -50,7 +53,6 @@ export interface ToolSectionLabel {
links?: Record<string, string> | null;
}
// TODO: Use this in ToolSearch.vue
export interface FilterSettings {
[key: string]: any;
name?: string;
@@ -89,8 +91,7 @@ export const useToolStore = defineStore("toolStore", () => {
return toolsById.value;
} else {
const q = createWhooshQuery(filterSettings);
console.log("getToolsById", toolResults.value[q]);
return filterTools(toolsById.value, toolResults.value[q]);
return filterTools(toolsById.value, toolResults.value[q] || []);
}
};
});
+1 -1
View File
@@ -7,7 +7,7 @@ import type { Steps } from "@/stores/workflowStepStore";
export interface Workflow {
name: string;
[index: string]: any;
id: string;
steps: Steps;
step_count?: number;
latest_id?: string;