mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-11-03 04:43:11 +08:00
improve typing and modernize tool panel components to TS/composition API
This commit is contained in:
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}")]'
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user