improve typing and modernize tool panel components to TS/composition API

This commit is contained in:
Ahmed Awan
2026-02-12 15:01:07 -06:00
parent 813ba21feb
commit 2bcb7381a5
11 changed files with 215 additions and 273 deletions
+15
View File
@@ -1,5 +1,6 @@
import { type components, GalaxyApi } from "@/api";
import { ERROR_STATES, type ShowFullJobResponse } from "@/api/jobs";
import type { Tool, ToolPanelItem, ToolSection, ToolSectionLabel } from "@/stores/toolStore";
import { errorMessageAsString } from "@/utils/simple-error";
export type HdcaUploadTarget = components["schemas"]["HdcaDataItemsTarget"];
@@ -130,3 +131,17 @@ export function fetchJobErrorMessage(jobDetails: ShowFullJobResponse): string |
}
return errorMessage;
}
// TODO: Once the backend models are typed, make sure these type guards are correct.
export function isTool(section: ToolPanelItem): section is Tool {
return !isToolSection(section) && !isToolSectionLabel(section);
}
export function isToolSection(section: ToolPanelItem): section is ToolSection {
return (section as ToolSection).tools !== undefined || (section as ToolSection).elems !== undefined;
}
export function isToolSectionLabel(section: ToolPanelItem): section is ToolSectionLabel {
return (
(section as ToolSectionLabel).text !== undefined &&
(section as ToolSectionLabel).model_class === "ToolSectionLabel"
);
}
+82 -121
View File
@@ -1,135 +1,96 @@
<template>
<div class="toolTitle">
<a v-if="tool.disabled" :data-tool-id="tool.id" class="title-link name text-muted tool-link">
<span v-if="!hideName">{{ tool.name }}</span>
<span class="description">{{ tool.description }}</span>
</a>
<a
v-else
:class="targetClass"
:data-tool-id="tool.id"
:href="toolLink"
:target="toolTarget"
:title="tool.help"
@click="onClick">
<span class="labels">
<span
v-for="(label, index) in tool.labels"
:key="index"
:class="['badge', 'badge-primary', `badge-${label}`]">
{{ label }}
</span>
</span>
<span v-if="!hideName" class="name font-weight-bold">{{ tool.name }}</span>
<span class="description">{{ tool.description }}</span>
<span
v-b-tooltip.hover
:class="['operation', 'float-right', operationIcon]"
:title="operationTitle"
@click.stop.prevent="onOperation" />
</a>
<ToolFavoriteButton
v-if="showFavoriteAction"
:id="tool.id"
:class="['tool-favorite-button', { 'tool-favorite-button-hover': showFavoriteOnHover }]"
:data-tool-id="tool.id"
color="grey" />
</div>
</template>
<script>
import BootstrapVue from "bootstrap-vue";
<script setup lang="ts">
import { storeToRefs } from "pinia";
import Vue, { computed } from "vue";
import { computed } from "vue";
import type { Tool as ToolType } from "@/stores/toolStore";
import { useToolStore } from "@/stores/toolStore";
import { useUserStore } from "@/stores/userStore";
import ariaAlert from "@/utils/ariaAlert";
import ToolFavoriteButton from "@/components/Tool/Buttons/ToolFavoriteButton.vue";
Vue.use(BootstrapVue);
interface Props {
tool: ToolType;
operationTitle?: string;
operationIcon?: string;
hideName?: boolean;
showFavoriteButton?: boolean;
}
export default {
name: "Tool",
components: {
ToolFavoriteButton,
},
props: {
tool: {
type: Object,
required: true,
},
operationTitle: {
type: String,
default: "",
},
operationIcon: {
type: String,
default: "",
},
hideName: {
type: Boolean,
default: false,
},
toolKey: {
type: String,
default: "",
},
renderIcon: {
type: Boolean,
default: false,
},
showFavoriteButton: {
type: Boolean,
default: false,
},
},
setup(props) {
const toolStore = useToolStore();
const userStore = useUserStore();
const { currentFavorites } = storeToRefs(userStore);
const isFavorite = computed(() => currentFavorites.value.tools?.includes(props.tool.id));
return {
getLinkById: toolStore.getLinkById,
getTargetById: toolStore.getTargetById,
isFavorite,
};
},
computed: {
toolLink() {
return this.getLinkById(this.tool.id);
},
toolTarget() {
return this.getTargetById(this.tool.id);
},
targetClass() {
if (this.toolKey) {
return `tool-menu-item-${this.tool[this.toolKey]} title-link cursor-pointer tool-link`;
} else {
return `title-link cursor-pointer tool-link`;
}
},
showFavoriteAction() {
return this.showFavoriteButton || this.isFavorite;
},
showFavoriteOnHover() {
return this.isFavorite;
},
},
methods: {
onClick(evt) {
ariaAlert(`${this.tool.name} selected from panel`);
this.$emit("onClick", this.tool, evt);
},
onOperation(evt) {
ariaAlert(`${this.tool.name} operation selected from panel`);
this.$emit("onOperation", this.tool, evt);
},
},
};
const props = withDefaults(defineProps<Props>(), {
operationTitle: "",
operationIcon: "",
hideName: false,
showFavoriteButton: false,
});
const emit = defineEmits<{
(e: "onClick", tool: ToolType, evt: MouseEvent): void;
(e: "onOperation", tool: ToolType, evt: MouseEvent | KeyboardEvent): void;
}>();
const toolStore = useToolStore();
const userStore = useUserStore();
const { currentFavorites } = storeToRefs(userStore);
const isFavorite = computed(() => currentFavorites.value.tools?.includes(props.tool.id));
const toolLink = computed(() => toolStore.getLinkById(props.tool.id));
const toolTarget = computed(() => toolStore.getTargetById(props.tool.id));
function onClick(evt: MouseEvent) {
ariaAlert(`${props.tool.name} selected from panel`);
emit("onClick", props.tool, evt);
}
function onOperation(evt: MouseEvent | KeyboardEvent) {
ariaAlert(`${props.tool.name} operation selected from panel`);
emit("onOperation", props.tool, evt);
}
</script>
<template>
<div class="toolTitle">
<a v-if="props.tool.disabled" :data-tool-id="props.tool.id" class="title-link name text-muted tool-link">
<span v-if="!props.hideName">{{ props.tool.name }}</span>
<span class="description">{{ props.tool.description }}</span>
</a>
<a
v-else
class="title-link cursor-pointer tool-link"
:data-tool-id="props.tool.id"
:href="toolLink"
:target="toolTarget"
:title="props.tool.help"
@click="onClick">
<span class="labels">
<span
v-for="(label, index) in props.tool.labels"
:key="index"
:class="['badge', 'badge-primary', `badge-${label}`]">
{{ label }}
</span>
</span>
<span v-if="!props.hideName" class="name font-weight-bold">{{ props.tool.name }}</span>
<span class="description">{{ props.tool.description }}</span>
<span
v-b-tooltip.hover
:class="['operation', 'float-right', props.operationIcon]"
role="button"
tabindex="0"
:title="props.operationTitle"
@keydown.enter.prevent="onOperation"
@click.stop.prevent="onOperation" />
</a>
<ToolFavoriteButton
v-if="props.showFavoriteButton || isFavorite"
:id="props.tool.id"
:class="['tool-favorite-button', { 'tool-favorite-button-hover': isFavorite }]"
:data-tool-id="props.tool.id"
color="grey" />
</div>
</template>
<style scoped>
.toolTitle {
display: flex;
@@ -88,7 +88,7 @@ function onKeydown(event: KeyboardEvent) {
@click.stop="clearRecentTools">
<FontAwesomeIcon :icon="faBroom" />
</GButton>
<ToolPanelLinks :links="definition.links || undefined" />
<ToolPanelLinks v-if="definition.links" :links="definition.links" />
</span>
</div>
</template>
@@ -1,3 +1,18 @@
<script setup lang="ts">
import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { computed } from "vue";
const props = defineProps<{
links: Record<string, string>;
}>();
const link = computed(() => {
const links = Object.values(props.links);
return links.length > 0 ? links[0] : null;
});
</script>
<template>
<span v-if="link" class="tool-panel-links">
<a :href="link" target="_blank" style="display: inline">
@@ -6,29 +21,3 @@
</a>
</span>
</template>
<script>
import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
export default {
components: { FontAwesomeIcon },
props: {
links: {
type: Object,
default: () => ({}),
},
},
data() {
return {
faExternalLinkAlt,
};
},
computed: {
link() {
const links = Object.values(this.links || {});
return links.length > 0 ? links[0] : null;
},
},
};
</script>
@@ -4,8 +4,9 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { useEventBus } from "@vueuse/core";
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { isTool, isToolSection, isToolSectionLabel } from "@/api/tools";
import { useConfig } from "@/composables/config";
import { type Tool as ToolType, type ToolSection, type ToolSectionLabel, useToolStore } from "@/stores/toolStore";
import { type Tool as ToolType, type ToolPanelItem, useToolStore } from "@/stores/toolStore";
import ariaAlert from "@/utils/ariaAlert";
import Tool from "./Tool.vue";
@@ -13,7 +14,7 @@ import ToolPanelLabel from "./ToolPanelLabel.vue";
import ToolPanelLinks from "./ToolPanelLinks.vue";
const emit = defineEmits<{
(e: "onClick", tool: any, evt: Event): void;
(e: "onClick", tool: ToolType, evt: Event): void;
(e: "onFilter", filter: string): void;
(e: "onOperation", tool: any, evt: Event): void;
(e: "onLabelToggle", labelId: string): void;
@@ -22,14 +23,12 @@ const emit = defineEmits<{
const eventBus = useEventBus<string>("open-tool-section");
interface Props {
category: ToolSection | ToolType | ToolSectionLabel;
category: ToolPanelItem;
queryFilter?: string;
disableFilter?: boolean;
hideName?: boolean;
operationTitle?: string;
operationIcon?: string;
toolKey?: string;
sectionName?: string;
expanded?: boolean;
sortItems?: boolean;
hasFilterButton?: boolean;
@@ -44,8 +43,6 @@ const props = withDefaults(defineProps<Props>(), {
hideName: false,
operationTitle: "",
operationIcon: "",
toolKey: "",
sectionName: "default",
expanded: false,
sortItems: true,
hasFilterButton: false,
@@ -57,38 +54,39 @@ const props = withDefaults(defineProps<Props>(), {
const { config, isConfigLoaded } = useConfig();
const toolStore = useToolStore();
/**
* We can have sections of 2 types:
* 1. Sections with `elems` property, which is an array of tools and sections.
* 2. Sections with `tools` property, which is an array of tool IDs (strings) and section labels.
* In this case, we need to resolve the tool IDs to get the actual tools.
*/
const elems = computed(() => {
if (toolSection.value.elems !== undefined && toolSection.value.elems.length > 0) {
return toolSection.value.elems.filter((el) => el !== null && el !== undefined);
}
if (toolSection.value.tools !== undefined && toolSection.value.tools.length > 0) {
return toolSection.value.tools
.map((toolId) => {
const tool = toolStore.getToolForId(toolId as string);
if (!tool && typeof toolId !== "string") {
return toolId as ToolSectionLabel;
} else {
return tool;
}
})
.filter((el) => el !== null && el !== undefined);
if (isToolSection(props.category)) {
if (props.category.elems !== undefined && props.category.elems.length > 0) {
// This section has `elems`, we can return it as is.
return props.category.elems;
}
if (props.category.tools !== undefined && props.category.tools.length > 0) {
return props.category.tools
.map((toolOrLabel) => {
if (typeof toolOrLabel === "string") {
// This is a tool ID, we need to resolve it to get the actual `Tool`.
return toolStore.getToolForId(toolOrLabel) || null;
} else {
// This is a `ToolSectionLabel`, we can return it as is.
return toolOrLabel;
}
})
.filter((el) => el !== null);
}
}
return [];
});
const toolSection = computed(() => props.category as ToolSection);
const toolSectionLabel = computed(() => props.category as ToolSectionLabel);
const name = computed(() => toolSection.value.title || toolSection.value.name);
const isSection = computed(() => toolSection.value.tools !== undefined || toolSection.value.elems !== undefined);
const hasElements = computed(() => elems.value.length > 0);
const title = computed(() => props.category.description || undefined);
const links = computed(() => toolSection.value.links || {});
const opened = ref(props.expanded || checkFilter());
const sectionToggleIcon = computed(() => (opened.value ? faChevronDown : faChevronRight));
const sortedElements = computed(() => {
const sortedElements = computed<Array<[string, ToolPanelItem]>>(() => {
// If this.config.sortTools is true, sort the tools alphabetically
// When administrators have manually inserted labels we respect
// the order set and hope for the best from the integrated
@@ -98,12 +96,12 @@ const sortedElements = computed(() => {
isConfigLoaded.value &&
config.value.toolbox_auto_sort === true &&
props.sortItems === true &&
!elems.value.some((el) => (el as ToolSectionLabel).text !== undefined && (el as ToolSectionLabel).text !== "")
!elems.value.some((el) => isToolSectionLabel(el) && el.text !== "")
) {
const elements = [...elems.value];
const sorted = elements.sort((a, b) => {
const aNameLower = (a as ToolSection).name.toLowerCase();
const bNameLower = (b as ToolSection).name.toLowerCase();
const aNameLower = "name" in a ? a.name.toLowerCase() : "";
const bNameLower = "name" in b ? b.name.toLowerCase() : "";
if (aNameLower > bNameLower) {
return 1;
} else if (aNameLower < bNameLower) {
@@ -128,9 +126,9 @@ watch(
watch(
() => opened.value,
(newVal: boolean, oldVal: boolean) => {
if (newVal !== oldVal) {
if (newVal !== oldVal && isToolSection(props.category)) {
const currentState = newVal ? "opened" : "closed";
ariaAlert(`${name.value} tools menu ${currentState}`);
ariaAlert(`${props.category.name} tools menu ${currentState}`);
}
},
);
@@ -144,14 +142,14 @@ onUnmounted(() => {
});
function openToolSection(sectionId: string) {
if (isSection.value && sectionId == props.category?.id) {
if (isToolSection(props.category) && sectionId == props.category?.id) {
toggleMenu(true);
}
}
function checkFilter() {
return !props.disableFilter && !!props.queryFilter;
}
function onClick(tool: any, evt: Event) {
function onClick(tool: ToolType, evt: Event) {
emit("onClick", tool, evt);
}
function onOperation(tool: any, evt: Event) {
@@ -169,11 +167,11 @@ function getCollapsedState(id: string): boolean | undefined {
</script>
<template>
<div v-if="isSection && hasElements" class="tool-panel-section">
<div v-if="isToolSection(props.category) && elems.length > 0" class="tool-panel-section">
<div
v-b-tooltip.topright.hover.noninteractive
:class="['toolSectionTitle', `tool-menu-section-${sectionName}`, 'tool-panel-divider']"
:title="title">
class="toolSectionTitle tool-panel-divider"
:title="props.category.description || undefined">
<a
class="title-link tool-panel-divider-link"
href="javascript:void(0)"
@@ -183,15 +181,15 @@ function getCollapsedState(id: string): boolean | undefined {
<span class="tool-panel-divider-text">
<FontAwesomeIcon :icon="sectionToggleIcon" class="tool-panel-divider-toggle" />
<span class="name">
{{ name }}
{{ props.category.title || props.category.name }}
</span>
<ToolPanelLinks :links="links" />
<ToolPanelLinks v-if="props.category.links" :links="props.category.links" />
<button
v-if="isSection && props.hasFilterButton"
v-if="props.hasFilterButton"
v-b-tooltip.hover.noninteractive.bottom
title="Show full section"
class="inline-icon-button"
@click.stop="emit('onFilter', `section:${toolSection.name}`)">
@click.stop="emit('onFilter', `section:${props.category.name}`)">
<FontAwesomeIcon :icon="faFilter" />
</button>
</span>
@@ -201,17 +199,16 @@ function getCollapsedState(id: string): boolean | undefined {
<div v-if="opened" data-description="opened tool panel section">
<template v-for="[key, el] in sortedElements">
<ToolPanelLabel
v-if="toolSectionLabel.text || el.model_class === 'ToolSectionLabel'"
v-if="isToolSectionLabel(el)"
:key="`label-${key}`"
:definition="el"
:collapsed="getCollapsedState(el.id)"
@toggle="onLabelToggle" />
<Tool
v-else
v-else-if="isTool(el)"
:key="`tool-${key}`"
class="ml-2"
:tool="el"
:tool-key="toolKey"
:hide-name="hideName"
:operation-title="operationTitle"
:operation-icon="operationIcon"
@@ -222,22 +219,20 @@ function getCollapsedState(id: string): boolean | undefined {
</div>
</transition>
</div>
<div v-else>
<ToolPanelLabel
v-if="toolSectionLabel.text"
:definition="toolSectionLabel"
:collapsed="getCollapsedState(toolSectionLabel.id)"
@toggle="onLabelToggle" />
<Tool
v-else
:tool="category"
:hide-name="hideName"
:operation-title="operationTitle"
:operation-icon="operationIcon"
:show-favorite-button="props.showFavoriteButton || searchActive"
@onOperation="onOperation"
@onClick="onClick" />
</div>
<ToolPanelLabel
v-else-if="isToolSectionLabel(props.category)"
:definition="props.category"
:collapsed="getCollapsedState(props.category.id)"
@toggle="onLabelToggle" />
<Tool
v-else-if="isTool(props.category)"
:tool="props.category"
:hide-name="hideName"
:operation-title="operationTitle"
:operation-icon="operationIcon"
:show-favorite-button="props.showFavoriteButton || searchActive"
@onOperation="onOperation"
@onClick="onClick" />
</template>
<style lang="scss" scoped>
+7 -9
View File
@@ -8,7 +8,7 @@ import { computed, type ComputedRef, type Ref, ref, watch } from "vue";
import { useGlobalUploadModal } from "@/composables/globalUploadModal";
import { useToolRouting } from "@/composables/route";
import { useFavoriteSearchResults, useToolPanelFavorites } from "@/composables/toolPanelFavorites";
import type { Tool, ToolSection as ToolSectionType, ToolSectionLabel } from "@/stores/toolStore";
import type { Tool, ToolPanelItem, ToolSection as ToolSectionType } from "@/stores/toolStore";
import { useToolStore } from "@/stores/toolStore";
import { useUserStore } from "@/stores/userStore";
import localize from "@/utils/localization";
@@ -132,8 +132,6 @@ const localSectionsById = computed(() => {
) as Record<string, Tool | ToolSectionType>;
});
type PanelItem = Tool | ToolSectionType | ToolSectionLabel;
// Use composable for favorites and recent tools management
const {
favoritesCollapsed,
@@ -189,14 +187,14 @@ const searchPanelSections = computed(() => {
const toolsList = computed(() => Object.values(localToolsById.value));
const flatResultsPanel = computed<Record<string, PanelItem> | null>(() => {
const flatResultsPanel = computed<Record<string, ToolPanelItem> | null>(() => {
if (!hasResults.value) {
return null;
}
if (!hasMixedResults.value) {
return filterTools(localToolsById.value, results.value) as Record<string, PanelItem>;
return filterTools(localToolsById.value, results.value) as Record<string, ToolPanelItem>;
}
const entries: Array<[string, PanelItem]> = [];
const entries: Array<[string, ToolPanelItem]> = [];
entries.push([
PANEL_LABEL_IDS.FAVORITES_RESULTS_LABEL,
buildToolLabel(PANEL_LABEL_IDS.FAVORITES_RESULTS_LABEL, localize("Favorites")),
@@ -212,11 +210,11 @@ const flatResultsPanel = computed<Record<string, PanelItem> | null>(() => {
return Object.fromEntries(entries);
});
const favoritesDefaultPanel = computed<Record<string, PanelItem> | null>(() => {
const favoritesDefaultPanel = computed<Record<string, ToolPanelItem> | null>(() => {
if (!props.favoritesDefault || query.value) {
return null;
}
const entries: Array<[string, PanelItem]> = [];
const entries: Array<[string, ToolPanelItem]> = [];
const favorites = favoriteToolIdsInPanel.value;
const recents = recentToolIdsToShow.value;
@@ -278,7 +276,7 @@ const sectionedResultsPanel = computed<Record<string, Tool | ToolSectionType> |
* If we have results for search, we show tools in sections or just tools,
* based on whether `showSections` is true or false
*/
const localPanel: ComputedRef<Record<string, PanelItem> | null> = computed(() => {
const localPanel: ComputedRef<Record<string, ToolPanelItem> | null> = computed(() => {
if (props.favoritesDefault && !query.value) {
return favoritesDefaultPanel.value || {};
}
@@ -36,7 +36,7 @@ const tempToolPanel = {
id: "fasta/fastq",
name: "FASTA/FASTQ",
},
},
} as unknown as Record<string, ToolSection>,
};
const tempToolsList = {
tools: {
+28 -44
View File
@@ -14,6 +14,7 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { orderBy } from "lodash";
import { isTool, isToolSection } from "@/api/tools";
import type { FilterSettings as ToolFilters, Tool, ToolSection, ToolSectionLabel } from "@/stores/toolStore";
import levenshteinDistance from "@/utils/levenshtein";
@@ -71,7 +72,7 @@ const FILTER_KEYS = {
const STRING_REPLACEMENTS: string[] = [" ", "-", "\\(", "\\)", "'", ":", `"`];
const MINIMUM_DL_LENGTH = 5; // for Demerau-Levenshtein distance
const MINIMUM_WORD_MATCH = 2; // for word match
const UNSECTIONED_SECTION = {
const UNSECTIONED_SECTION: ToolSection = {
// to return a section for unsectioned tools
model_class: "ToolSection",
id: "unsectioned",
@@ -243,25 +244,28 @@ export function getValidToolsInCurrentView(
export function getValidToolsInEachSection(
validToolIdsInCurrentView: string[],
currentPanel: Record<string, Tool | ToolSection>,
) {
): Array<[string, Tool | ToolSection]> {
// use a set for fast membership lookup
const idSet = new Set(validToolIdsInCurrentView);
return Object.entries(currentPanel).map(([id, section]) => {
const validatedSection = { ...section } as ToolSection;
// assign sectionTools to avoid repeated getter access
const sectionTools = validatedSection.tools;
if (sectionTools && Array.isArray(sectionTools)) {
// filter on valid tools and panel labels in this section
validatedSection.tools = sectionTools.filter((toolId) => {
if (typeof toolId === "string" && idSet.has(toolId)) {
return true;
} else if (typeof toolId !== "string") {
// is a special case where there is a label within a section
return true;
}
});
if (isToolSection(section)) {
const validatedSection = { ...section };
// assign sectionTools to avoid repeated getter access
const sectionTools = validatedSection.tools;
if (sectionTools && Array.isArray(sectionTools)) {
// filter on valid tools and panel labels in this section
validatedSection.tools = sectionTools.filter((toolId) => {
if (typeof toolId === "string" && idSet.has(toolId)) {
return true;
} else if (typeof toolId !== "string") {
// is a special case where there is a label within a section
return true;
}
});
}
return [id, validatedSection];
}
return [id, validatedSection];
return [id, section];
});
}
@@ -272,20 +276,18 @@ export function getValidToolsInEachSection(
* @returns a `currentPanel` object containing sections/tools/labels that meet required conditions
*/
export function getValidPanelItems(
items: (string | ToolSection)[][],
items: Array<[string, ToolSection | Tool]>,
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)) {
if (isTool(item) && validToolIdsInCurrentView.includes(id)) {
// is a `Tool` and is in `localToolsById`
return true;
} else if (item.tools === undefined) {
} else if (!isToolSection(item)) {
// is neither a `Tool` nor a `ToolSection`, maybe a `ToolSectionLabel`
return true;
} else if (item.tools && item.tools.length > 0 && !excludedSectionIds.includes(id)) {
} else if ("tools" in item && item.tools?.length && !excludedSectionIds.includes(id)) {
// is a `ToolSection` with tools; is not an excluded section
return true;
} else {
@@ -483,12 +485,9 @@ export function createSortedResultPanel(matchedTools: SearchMatch[], currentPane
const 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)
) {
const existingPanelItem = currentPanel[section];
if (existingPanelItem) {
if ("tools" in existingPanelItem && existingPanelItem.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
@@ -500,7 +499,7 @@ export function createSortedResultPanel(matchedTools: SearchMatch[], currentPane
existingSection.tools?.push(match.id);
acc[section] = existingSection;
toolAdded = true;
} else if (isToolObject(existingPanelItem as Tool) && existingPanelItem.id === match.id) {
} else if (isTool(existingPanelItem) && 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)
@@ -563,21 +562,6 @@ function closestSubstring(query: string, actualStr: string) {
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);
+7 -3
View File
@@ -37,6 +37,7 @@ export interface Panel {
searchable: boolean;
}
// TODO: Once the backend models are typed, we will replace these with the generated types from the schema.
export interface Tool {
model_class: string;
id: string;
@@ -64,10 +65,11 @@ export interface Tool {
changeset_revision: string;
tool_shed: string;
};
help?: string;
}
export interface ToolSection {
model_class: string;
model_class: "ToolSection";
id: string;
name: string;
title?: string;
@@ -75,11 +77,11 @@ export interface ToolSection {
description?: string;
links?: Record<string, string>;
tools?: (string | ToolSectionLabel)[];
elems?: (Tool | ToolSection)[];
elems?: (Tool | ToolSection)[]; // TODO: Are we sure that a `ToolSection` can have `ToolSection` children?
}
export interface ToolSectionLabel {
model_class: string;
model_class: "ToolSectionLabel";
id: string;
text: string;
version?: string;
@@ -87,6 +89,8 @@ export interface ToolSectionLabel {
links?: Record<string, string> | null;
}
export type ToolPanelItem = Tool | ToolSection | ToolSectionLabel;
export type ToolHelpData = {
help?: string;
summary?: string;
@@ -939,8 +939,6 @@ workflow_editor:
selector: 'license save'
type: data-description
tool_menu: '.toolMenuContainer'
tool_menu_section_link: '.tool-menu-section-${section_name} a span'
tool_menu_item_link: 'a.tool-menu-item-${item_name}'
workflow_link:
type: xpath
selector: '//a[contains(., "${workflow_title}")]'
-2
View File
@@ -447,8 +447,6 @@ interface Rootworkflow_editor extends Component {
edit_annotation: SelectorTemplate;
edit_name: SelectorTemplate;
tool_menu: SelectorTemplate;
tool_menu_section_link: SelectorTemplate;
tool_menu_item_link: SelectorTemplate;
workflow_link: SelectorTemplate;
insert_steps: SelectorTemplate;
connect_icon: SelectorTemplate;