Drop ToolsView, which is be superseded by the advanced search results view. The central panel search result view has a similar display but improved loading/filtering characteristics and we could use it in a 'show all the tools' display like this as well.

This commit is contained in:
Dannon Baker
2022-09-28 22:25:16 -04:00
parent 5c68bba05c
commit cb9723b150
5 changed files with 1 additions and 295 deletions
@@ -9,7 +9,7 @@ import flushPromises from "flush-promises";
const localVue = getLocalVue();
describe("ToolsView/ToolsView.vue", () => {
describe("ToolSchemaJson/ToolsView.vue", () => {
let wrapper;
let axiosMock;
const defaultSchemaElementTag = "application/ld+json";
@@ -1,77 +0,0 @@
import ToolsView from "./ToolsView";
import { mount } from "@vue/test-utils";
import { getLocalVue } from "jest/helpers";
import flushPromises from "flush-promises";
// test response
import testToolsListResponse from "./testData/toolsList";
import testCitation from "./testData/citation";
import MockAdapter from "axios-mock-adapter";
import axios from "axios";
jest.mock("app");
describe("ToolsView/ToolsView.vue", () => {
const localVue = getLocalVue();
let wrapper;
let axiosMock;
beforeEach(async () => {
axiosMock = new MockAdapter(axios);
axiosMock.onGet("/api/tools?tool_help=True").reply(200, testToolsListResponse);
axiosMock.onGet(new RegExp(`./*/citations`)).reply(200, testCitation);
wrapper = mount(ToolsView, { localVue, attachTo: document.body });
await flushPromises();
});
afterEach(() => {
axiosMock.restore();
});
it("should render infinite scroll div", async () => {
expect(wrapper.html()).toEqual(expect.stringContaining('<div infinite-scroll-disabled="busy">'));
});
it("should return defined number of tools", async () => {
expect(wrapper.vm.getToolsNumber() === 5).toBeTruthy();
});
it("should render only specific number of tools, equal to current buffer", async () => {
const buttons = wrapper.findAll('[type="button"]').filter((button) => button.text() === "Info");
// one 'info' button per tool
expect(wrapper.vm.buffer.length === buttons.length).toBeTruthy();
});
it("should open modal on button click", async () => {
// findAll() returns WrapperArray, thus regular array.find() won't work
const infoButton = wrapper
.findAll('[type="button"]')
.filter((button) => button.text() === "Info")
.at(0);
const modalId = "modal--" + infoButton.attributes().index;
const modal = wrapper.find("#" + modalId);
expect(modal.element).not.toBeVisible();
await infoButton.trigger("click");
await flushPromises();
expect(modal.element).toBeVisible();
});
it("citation should open on click", async () => {
const infoButton = wrapper
.findAll('[type="button"]')
.filter((button) => button.text() === "Citations")
.at(0);
const citation = wrapper.find("#" + infoButton.attributes("aria-controls").replace(/ /g, "_"));
expect(citation.element).not.toBeVisible();
expect(infoButton.attributes("aria-expanded") === "false").toBeTruthy();
await infoButton.trigger("click");
await flushPromises();
expect(infoButton.attributes("aria-expanded") === "true").toBeTruthy();
expect(citation.element).toBeVisible();
});
});
@@ -1,211 +0,0 @@
<template>
<div v-infinite-scroll="loadMore" infinite-scroll-disabled="busy">
<h2 class="mb-3" style="text-align: center">
<span id="tools-view">Consolidated view of {{ tools.length }} available tools.</span>
</h2>
<div v-if="!loading">
<b-container fluid class="mb-4">
<b-row class="justify-content-center">
<b-col md="6">
<b-form-group description="Search for strings or regular expressions">
<b-input-group>
<b-form-input
v-model="filterText"
placeholder="Type to Search"
@keyup.native="filter('filterByText')"
@keyup.esc.native="filterText = ''" />
<b-input-group-append>
<b-btn :disabled="!filter" @click="filter = ''">Clear (esc)</b-btn>
</b-input-group-append>
</b-input-group>
</b-form-group>
</b-col>
</b-row>
</b-container>
<isotope
ref="iso"
:options="isoOptions"
:list="buffer"
style="margin: 0 auto"
@filter="filterOption = arguments[2]">
<b-card v-for="(info, index) in buffer" :key="index" ref="cards" class="m-2" style="width: 23rem">
<template v-slot:header>
<div>
<b-link :href="info.url" target="_blank">
<h4 class="tools-view-name">{{ info.name }}</h4>
</b-link>
<b-badge class="tools-view-section">{{ info.section }}</b-badge>
</div>
</template>
<p class="card-text" v-html="helpSummary(info.help) || info.description" />
<p class="card-text">
<b-btn v-b-modal="'modal-' + '-' + index" :index="index">Info</b-btn>
<b-modal :id="'modal-' + '-' + index" centered ok-only :static="true" :title="info.name">
<b>{{ info.version + " / " + info.id }}</b>
<p>{{ info.description }}</p>
<p v-html="info.help"></p>
</b-modal>
</p>
<Citations
:id="info.id"
simple
source="tools"
@rendered="layout"
@show="show(index)"
@shown="shown(index)"
@hidden="hidden(index)" />
</b-card>
</isotope>
</div>
</div>
</template>
<script>
import { getAppRoot } from "onload/loadConfig";
import infiniteScroll from "vue-infinite-scroll";
import isotope from "vueisotope";
import axios from "axios";
import Citations from "components/Citation/Citations.vue";
import { setTimeout } from "timers";
export default {
components: {
Citations,
isotope,
},
directives: { infiniteScroll },
props: {
transitionDuration: {
type: Number,
default: 200,
},
},
data() {
return {
tools: [],
buffer: [],
filterOption: null,
filterText: "",
busy: false,
loading: true,
};
},
computed: {
isoOptions() {
return {
transitionDuration: this.transitionDuration,
masonry: {
fitWidth: true,
},
getFilterData: {
filterByText: (el) => {
const re = new RegExp(this.filterText, "i");
return el.name.match(re) || el.description.match(re) || el.help.match(re);
},
},
};
},
},
created() {
axios
.get(`${getAppRoot()}api/tools?tool_help=True`)
.then((response) => {
this.initialize(response.data);
})
.catch((error) => {
console.error(error);
});
},
methods: {
show(index) {
this.$refs.cards[index].style.zIndex = "1";
},
shown(index) {
this.layout();
setTimeout(() => {
this.$refs.cards[index].style.zIndex = "auto";
}, 200);
},
hidden(index) {
this.layout();
},
layout() {
this.$refs.iso.layout();
},
filter(key) {
this.$refs.iso.filter(key);
},
toolsExtracted(tools) {
function extractSections(acc, section) {
function extractTools(_acc, tool) {
return tool.name
? [
..._acc,
{
id: tool.id,
name: tool.name,
section: section.name,
description: tool.description,
url: getAppRoot() + String(tool.link).substring(1),
version: tool.version,
help: tool.help,
},
]
: _acc;
}
if ("elems" in section) {
return acc.concat(section.elems.reduce(extractTools, []));
}
return acc;
}
return tools
.reduce(extractSections, [])
.map((a) => [Math.random(), a])
.sort((a, b) => a[0] - b[0])
.map((a) => a[1]);
},
loadMore() {
if (this.buffer.length < this.tools.length) {
this.busy = true;
setTimeout(() => {
const start = this.buffer.length;
const end = start + 10;
const newItems = this.tools.slice(start, end);
this.buffer = this.buffer.concat(newItems);
this.busy = false;
}, 100);
}
},
helpSummary(help) {
const parser = new DOMParser();
const helpDoc = parser.parseFromString(help, "text/html");
const xpath = "//strong[text()='What it does']/../following-sibling::*";
const match = helpDoc.evaluate(
xpath,
helpDoc,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
if (match) {
return match.innerHTML;
}
const helpText = helpDoc.documentElement.textContent;
if (helpText) {
return helpText.substring(0, helpText.indexOf("\n\n"));
}
return null;
},
getToolsNumber() {
return this.tools.length;
},
initialize(tools) {
this.tools = this.toolsExtracted(tools);
this.buffer = this.tools.slice(0, 20);
this.loading = false;
},
},
};
</script>
@@ -1 +0,0 @@
[{"content": " @article{Blankenberg_2011, title={Making whole genome multiple alignments usable for biologists}, volume={27}, ISSN={1367-4803}, url={http://dx.doi.org/10.1093/bioinformatics/btr398}, DOI={10.1093/bioinformatics/btr398}, number={17}, journal={Bioinformatics}, publisher={Oxford University Press (OUP)}, author={Blankenberg, Daniel and Taylor, James and Nekrutenko, Anton}, year={2011}, month={Jul}, pages={2426\u00e2\u0080\u00932428}}\n", "format": "bibtex"}]
-5
View File
@@ -40,7 +40,6 @@ import Sharing from "components/Sharing/Sharing";
import StoredWorkflowInvocations from "components/Workflow/StoredWorkflowInvocations";
import ToolAdvancedSearch from "components/Panels/Common/ToolAdvancedSearch";
import ToolsJson from "components/ToolsView/ToolsSchemaJson/ToolsJson";
import ToolsView from "components/ToolsView/ToolsView";
import TourList from "components/Tour/TourList";
import TourRunner from "components/Tour/TourRunner";
import TrsImport from "components/Workflow/TrsImport";
@@ -298,10 +297,6 @@ export function getRouter(Galaxy) {
};
},
},
{
path: "tools/view",
component: ToolsView,
},
{
path: "tools/json",
component: ToolsJson,