From 33a74cae2def618e9fe32d1a8b1355d00a39a525 Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Tue, 22 Jan 2019 15:15:55 -0500 Subject: [PATCH 01/15] Fix hotdata method access; this still needs a lot of love/refactoring, but this makes it work again? --- .../components/RuleCollectionBuilder.vue | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/client/galaxy/scripts/components/RuleCollectionBuilder.vue b/client/galaxy/scripts/components/RuleCollectionBuilder.vue index 7136d150a54..6d527df80e3 100644 --- a/client/galaxy/scripts/components/RuleCollectionBuilder.vue +++ b/client/galaxy/scripts/components/RuleCollectionBuilder.vue @@ -1257,8 +1257,9 @@ export default { return targets; }, colHeaders() { - const data = this.hotData["data"]; - const columns = this.hotData["columns"]; + let hotData = this.hotData(); + const data = hotData["data"]; + const columns = hotData["columns"]; return RuleDefs.colHeadersFor(data, columns); }, colHeadersDisplay() { @@ -1684,7 +1685,8 @@ export default { return identifierColumns; }, buildRequestElements(createDatasetDescription, createSubcollectionDescription, subElementProp) { - const data = this.hotData["data"]; + let hotData = this.hotData(); + const data = hotData["data"]; const identifierColumns = this.identifierColumns(); if (identifierColumns.length < 1) { console.log("Error but this shouldn't have happened, create button should have been disabled."); @@ -1791,8 +1793,9 @@ export default { return elementsByName; }, creationElementsFromDatasets() { - const sources = this.hotData["sources"]; - const data = this.hotData["data"]; + let hotData = this.hotData(); + const sources = hotData["sources"]; + const data = hotData["data"]; const mappingAsDict = this.mappingAsDict; const elementsByCollectionName = this.buildRequestElements( @@ -1811,7 +1814,8 @@ export default { }, creationElementsForFetch() { // fetch elements for HDCA - const data = this.hotData["data"]; + let hotData = this.hotData(); + const data = hotData["data"]; const mappingAsDict = this.mappingAsDict; const elementsByCollectionName = this.buildRequestElements( @@ -1830,7 +1834,8 @@ export default { }, creationDatasetsForFetch() { // fetch elements for HDAs if not collection information specified. - const data = this.hotData["data"]; + let hotData = this.hotData(); + const data = hotData["data"]; const mappingAsDict = this.mappingAsDict; const datasets = []; From a64a5cdc5e1ace5981452026c4aa2857fbfd729b Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Tue, 22 Jan 2019 17:12:38 -0500 Subject: [PATCH 02/15] Prefer const, none of these are reassigned. --- .../scripts/components/RuleCollectionBuilder.vue | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/galaxy/scripts/components/RuleCollectionBuilder.vue b/client/galaxy/scripts/components/RuleCollectionBuilder.vue index 6d527df80e3..2d9a8028969 100644 --- a/client/galaxy/scripts/components/RuleCollectionBuilder.vue +++ b/client/galaxy/scripts/components/RuleCollectionBuilder.vue @@ -1257,7 +1257,7 @@ export default { return targets; }, colHeaders() { - let hotData = this.hotData(); + const hotData = this.hotData(); const data = hotData["data"]; const columns = hotData["columns"]; return RuleDefs.colHeadersFor(data, columns); @@ -1685,7 +1685,7 @@ export default { return identifierColumns; }, buildRequestElements(createDatasetDescription, createSubcollectionDescription, subElementProp) { - let hotData = this.hotData(); + const hotData = this.hotData(); const data = hotData["data"]; const identifierColumns = this.identifierColumns(); if (identifierColumns.length < 1) { @@ -1793,7 +1793,7 @@ export default { return elementsByName; }, creationElementsFromDatasets() { - let hotData = this.hotData(); + const hotData = this.hotData(); const sources = hotData["sources"]; const data = hotData["data"]; const mappingAsDict = this.mappingAsDict; @@ -1814,7 +1814,7 @@ export default { }, creationElementsForFetch() { // fetch elements for HDCA - let hotData = this.hotData(); + const hotData = this.hotData(); const data = hotData["data"]; const mappingAsDict = this.mappingAsDict; @@ -1834,7 +1834,7 @@ export default { }, creationDatasetsForFetch() { // fetch elements for HDAs if not collection information specified. - let hotData = this.hotData(); + const hotData = this.hotData(); const data = hotData["data"]; const mappingAsDict = this.mappingAsDict; From 3c9f263c69c657f8a644fceb778a7ff36c888d0a Mon Sep 17 00:00:00 2001 From: Mason Date: Wed, 23 Jan 2019 13:20:29 -0800 Subject: [PATCH 03/15] Restored hotData to computed, but removed unnecessary side-effects --- .../components/RuleCollectionBuilder.vue | 95 +++++++++---------- .../scripts/mvc/rules/rule-definitions.js | 6 +- 2 files changed, 48 insertions(+), 53 deletions(-) diff --git a/client/galaxy/scripts/components/RuleCollectionBuilder.vue b/client/galaxy/scripts/components/RuleCollectionBuilder.vue index 2d9a8028969..05a548841fb 100644 --- a/client/galaxy/scripts/components/RuleCollectionBuilder.vue +++ b/client/galaxy/scripts/components/RuleCollectionBuilder.vue @@ -448,7 +448,7 @@ []); + columns = []; + } else if (this.elementsType == "collection_contents") { + const collection = this.initialElements.slice(); + if (collection) { + const obj = this.populateElementsFromCollectionDescription( + collection.elements, + collection.collection_type + ); + data = obj.data; + sources = obj.sources; + columns = []; + } else { + data = []; + sources = []; + columns = []; + } + } else { + data = this.initialElements.slice(); + sources = data.map(el => null); + columns = []; + if (this.initialElements) { + this.initialElements[0].forEach(() => columns.push("new")); + } + } + return RuleDefs.applyRules(data, sources, columns, this.rules); + }, + colHeadersPerRule() { + return this.hotData.colHeadersPerRule; } }, methods: { @@ -1413,42 +1449,6 @@ export default { this.rules.push(rule); } }, - hotData() { - let data, sources, columns; - if ( - this.elementsType == "datasets" || - this.elementsType == "library_datasets" || - this.elementsType == "ftp" - ) { - data = this.initialElements.map(el => []); - sources = this.initialElements.slice(); - columns = []; - } else if (this.elementsType == "collection_contents") { - const collection = this.initialElements; - if (collection) { - const obj = this.populateElementsFromCollectionDescription( - collection.elements, - collection.collection_type - ); - data = obj.data; - sources = obj.sources; - columns = []; - } else { - data = []; - sources = []; - columns = []; - } - } else { - data = this.initialElements.slice(); - sources = data.map(el => null); - columns = []; - if (this.initialElements) { - this.initialElements[0].forEach(() => columns.push("new")); - } - } - this.colHeadersPerRule = []; - return RuleDefs.applyRules(data, sources, columns, this.rules, this.colHeadersPerRule); - }, viewSource() { this.resetSource(); this.ruleView = "source"; @@ -1685,8 +1685,7 @@ export default { return identifierColumns; }, buildRequestElements(createDatasetDescription, createSubcollectionDescription, subElementProp) { - const hotData = this.hotData(); - const data = hotData["data"]; + const data = this.hotData.data; const identifierColumns = this.identifierColumns(); if (identifierColumns.length < 1) { console.log("Error but this shouldn't have happened, create button should have been disabled."); @@ -1793,9 +1792,7 @@ export default { return elementsByName; }, creationElementsFromDatasets() { - const hotData = this.hotData(); - const sources = hotData["sources"]; - const data = hotData["data"]; + const { sources, data } = this.hotData; const mappingAsDict = this.mappingAsDict; const elementsByCollectionName = this.buildRequestElements( @@ -1814,8 +1811,7 @@ export default { }, creationElementsForFetch() { // fetch elements for HDCA - const hotData = this.hotData(); - const data = hotData["data"]; + const data = this.hotData.data; const mappingAsDict = this.mappingAsDict; const elementsByCollectionName = this.buildRequestElements( @@ -1834,8 +1830,7 @@ export default { }, creationDatasetsForFetch() { // fetch elements for HDAs if not collection information specified. - const hotData = this.hotData(); - const data = hotData["data"]; + const data = this.hotData.data; const mappingAsDict = this.mappingAsDict; const datasets = []; diff --git a/client/galaxy/scripts/mvc/rules/rule-definitions.js b/client/galaxy/scripts/mvc/rules/rule-definitions.js index 4b410cb2e55..c0cb365f121 100644 --- a/client/galaxy/scripts/mvc/rules/rule-definitions.js +++ b/client/galaxy/scripts/mvc/rules/rule-definitions.js @@ -861,8 +861,8 @@ const colHeadersFor = function(data, columns) { } }; -const applyRules = function(data, sources, columns, rules, colHeadersPerRule) { - colHeadersPerRule = colHeadersPerRule || []; +const applyRules = function(data, sources, columns, rules, headersPerRule = []) { + let colHeadersPerRule = Array.from(headersPerRule); let hasRuleError = false; for (var ruleIndex in rules) { const ruleHeaders = colHeadersFor(data, columns); @@ -889,7 +889,7 @@ const applyRules = function(data, sources, columns, rules, colHeadersPerRule) { columns = res.columns || columns; } } - return { data, sources, columns }; + return { data, sources, columns, colHeadersPerRule }; }; export default { From 205cdd0954ffdf34efc2c364ec1b62079e4a27d8 Mon Sep 17 00:00:00 2001 From: Mason Date: Wed, 23 Jan 2019 14:38:45 -0800 Subject: [PATCH 04/15] Removed slice method on non-array property --- client/galaxy/scripts/components/RuleCollectionBuilder.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/galaxy/scripts/components/RuleCollectionBuilder.vue b/client/galaxy/scripts/components/RuleCollectionBuilder.vue index 05a548841fb..1a83bdbe670 100644 --- a/client/galaxy/scripts/components/RuleCollectionBuilder.vue +++ b/client/galaxy/scripts/components/RuleCollectionBuilder.vue @@ -1385,7 +1385,7 @@ export default { data = sources.map(el => []); columns = []; } else if (this.elementsType == "collection_contents") { - const collection = this.initialElements.slice(); + const collection = this.initialElements; if (collection) { const obj = this.populateElementsFromCollectionDescription( collection.elements, From 569ea9ea36e15afcbd174b50ae2de79029c4bc0a Mon Sep 17 00:00:00 2001 From: Mason Date: Wed, 23 Jan 2019 14:56:53 -0800 Subject: [PATCH 05/15] Removed commented code in RuleCollectionBuilder --- client/galaxy/scripts/components/RuleCollectionBuilder.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/client/galaxy/scripts/components/RuleCollectionBuilder.vue b/client/galaxy/scripts/components/RuleCollectionBuilder.vue index 1a83bdbe670..47818d25224 100644 --- a/client/galaxy/scripts/components/RuleCollectionBuilder.vue +++ b/client/galaxy/scripts/components/RuleCollectionBuilder.vue @@ -1029,7 +1029,6 @@ export default { } return { rules: rules, - // colHeadersPerRule: [], mapping: mapping, state: "build", // 'build', 'error', 'wait', ruleView: "normal", // 'normal' or 'source' From f729dab31312d1be5ef2226d533d4ad26fb25130 Mon Sep 17 00:00:00 2001 From: Mason Date: Wed, 23 Jan 2019 18:36:41 -0800 Subject: [PATCH 06/15] Tagging component --- client/.babelrc | 12 - client/galaxy/scripts/bundleEntries.js | 2 +- .../components/Tags/GalaxyTags.test.js | 49 +++ .../scripts/components/Tags/GalaxyTags.vue | 162 ++++++++ .../components/Tags/StandardTags.test.js | 112 ++++++ .../scripts/components/Tags/StandardTags.vue | 127 ++++++ .../Tags}/autocomplete_tagging.scss | 0 .../galaxy/scripts/components/Tags/index.js | 3 + .../galaxy/scripts/components/Tags/inits.js | 12 + .../galaxy/scripts/components/Tags/model.js | 46 +++ .../scripts/components/Tags/model.test.js | 60 +++ .../scripts/components/Tags/tagService.js | 111 ++++++ .../components/Tags/tagService.test.js | 152 ++++++++ .../scripts/components/Tags/tagStyles.scss | 74 ++++ .../Tags/testData/autocompleteResponse.txt | 3 + .../scripts/entry/analysis/AnalysisRouter.js | 3 +- client/galaxy/scripts/mvc/grid/grid-view.js | 66 +++- client/galaxy/scripts/sample.test.js | 12 - .../galaxy/scripts/store/gridSearchStore.js | 33 ++ client/galaxy/scripts/store/index.js | 17 + client/galaxy/scripts/ui/autocom_tagging.js | 369 ------------------ client/galaxy/scripts/unitTestBundle.js | 13 - client/galaxy/scripts/utils/mock.js | 2 +- .../galaxy/scripts/utils/mountVueComponent.js | 11 + client/galaxy/scripts/utils/redirect.js | 5 + client/galaxy/scripts/viz/trackster.js | 1 - client/galaxy/style/scss/base.scss | 1 - client/galaxy/style/scss/mixins.scss | 8 + client/karma/karma.config.mocha.js | 36 +- client/karma/karma.config.qunit.js | 3 +- client/karma/webpack.config.unittest.js | 55 ++- client/package.json | 20 +- client/webpack.config.js | 27 +- client/yarn.lock | 159 +++++++- lib/galaxy/web/framework/helpers/grids.py | 4 +- lib/galaxy/webapps/galaxy/controllers/tag.py | 2 - lib/galaxy/webapps/reports/framework/grids.py | 4 +- templates/tagging_common.mako | 305 ++++----------- templates/webapps/galaxy/workflow/editor.mako | 2 +- .../test_published_histories_grid.py | 2 +- test/selenium_tests/test_saved_histories.py | 10 +- 41 files changed, 1366 insertions(+), 729 deletions(-) delete mode 100644 client/.babelrc create mode 100644 client/galaxy/scripts/components/Tags/GalaxyTags.test.js create mode 100644 client/galaxy/scripts/components/Tags/GalaxyTags.vue create mode 100644 client/galaxy/scripts/components/Tags/StandardTags.test.js create mode 100644 client/galaxy/scripts/components/Tags/StandardTags.vue rename client/galaxy/{style/scss => scripts/components/Tags}/autocomplete_tagging.scss (100%) create mode 100644 client/galaxy/scripts/components/Tags/index.js create mode 100644 client/galaxy/scripts/components/Tags/inits.js create mode 100644 client/galaxy/scripts/components/Tags/model.js create mode 100644 client/galaxy/scripts/components/Tags/model.test.js create mode 100644 client/galaxy/scripts/components/Tags/tagService.js create mode 100644 client/galaxy/scripts/components/Tags/tagService.test.js create mode 100644 client/galaxy/scripts/components/Tags/tagStyles.scss create mode 100644 client/galaxy/scripts/components/Tags/testData/autocompleteResponse.txt delete mode 100644 client/galaxy/scripts/sample.test.js create mode 100644 client/galaxy/scripts/store/gridSearchStore.js create mode 100644 client/galaxy/scripts/store/index.js delete mode 100644 client/galaxy/scripts/ui/autocom_tagging.js delete mode 100644 client/galaxy/scripts/unitTestBundle.js create mode 100644 client/galaxy/scripts/utils/mountVueComponent.js create mode 100644 client/galaxy/scripts/utils/redirect.js create mode 100644 client/galaxy/style/scss/mixins.scss diff --git a/client/.babelrc b/client/.babelrc deleted file mode 100644 index f9d881a541d..00000000000 --- a/client/.babelrc +++ /dev/null @@ -1,12 +0,0 @@ -{ - "presets": [ - [ - "@babel/preset-env", - { - "modules": false - } - ] - ], - "plugins": ["transform-vue-template", "@babel/plugin-syntax-dynamic-import"], - "ignore": ["i18n.js", "utils/localization.js", "nls/*"] -} diff --git a/client/galaxy/scripts/bundleEntries.js b/client/galaxy/scripts/bundleEntries.js index c630dbb4dfc..eefb1ccf33c 100644 --- a/client/galaxy/scripts/bundleEntries.js +++ b/client/galaxy/scripts/bundleEntries.js @@ -96,7 +96,7 @@ export let chartUtilities = { export { initMasthead } from "components/Masthead/initMasthead"; export { panelManagement } from "onload/globalInits/panelManagement"; -export { init_tag_click_function } from "ui/autocom_tagging"; +export { mountTaggingComponent } from "components/Tags"; // Used in common.mako export { default as store } from "store"; diff --git a/client/galaxy/scripts/components/Tags/GalaxyTags.test.js b/client/galaxy/scripts/components/Tags/GalaxyTags.test.js new file mode 100644 index 00000000000..624dae8ce84 --- /dev/null +++ b/client/galaxy/scripts/components/Tags/GalaxyTags.test.js @@ -0,0 +1,49 @@ +import { mount } from "@vue/test-utils"; +import GalaxyTags from "./GalaxyTags"; + +describe("Tags/GalaxyTags.vue", () => { + + const testTags = ["abc", "def", "ghi"]; + + let wrapper, emitted; + + beforeEach(function () { + wrapper = mount(GalaxyTags); + wrapper.setProps({ + value: testTags + }); + emitted = wrapper.emitted(); + }) + + it("should render a div for each tag", () => { + let tags = wrapper.findAll(".ti-tag-center"); + assert(tags.length == testTags.length, "Wrong number of tags"); + for(let i=0; i < testTags.length; i++) { + assert(tags.at(i).is('div'), "button not a div"); + assert(tags.at(i).text() == testTags[i], "rendered tag label doesn't match test data"); + } + }) + + it("should emit a click event when the tag is clicked", () => { + let tags = wrapper.findAll(".ti-tag-center > div"); + tags.at(0).trigger("click"); + assert(emitted["tag-click"], "click event not detected"); + assert(emitted["tag-click"].length == 1, "wrong event count"); + }) + + it("should emit a tag model payload when tag is clicked", () => { + let tags = wrapper.findAll(".ti-tag-center > div"); + tags.at(0).trigger("click"); + let firstEvent = emitted["tag-click"][0]; + let firstArg = firstEvent[0]; + assert(firstArg.text = testTags[0], "returned tag model doesn't match test data"); + }) + + it("should change intermal model representation when new tag list assigned", async () => { + assert(wrapper.vm.tagModels.length == 3); + let newTags = ["floob", "clown", "hoohah", "doodoo"]; + wrapper.setProps({ value: newTags }); + assert(wrapper.vm.tagModels.length == newTags.length); + }) + +}) diff --git a/client/galaxy/scripts/components/Tags/GalaxyTags.vue b/client/galaxy/scripts/components/Tags/GalaxyTags.vue new file mode 100644 index 00000000000..fd714eb3412 --- /dev/null +++ b/client/galaxy/scripts/components/Tags/GalaxyTags.vue @@ -0,0 +1,162 @@ + + + + + + + diff --git a/client/galaxy/scripts/components/Tags/StandardTags.test.js b/client/galaxy/scripts/components/Tags/StandardTags.test.js new file mode 100644 index 00000000000..9f8edfc1d1f --- /dev/null +++ b/client/galaxy/scripts/components/Tags/StandardTags.test.js @@ -0,0 +1,112 @@ +import sinon from "sinon"; +import { mount } from "@vue/test-utils"; +import StandardTags from "./StandardTags"; +import store from "../../store"; + +let mockRedirect = sinon.stub(); +StandardTags.__Rewire__("redirectToUrl", mockRedirect); + +describe("Tags/StandardTags.vue", () => { + + let wrapper; + let testTags = ["abc", "def", "ghi"]; + + function clickFirstTag() { + let firstTag = wrapper.find(".ti-tag-center > div"); + firstTag.trigger('click'); + } + + // TODO: put this in a store-specific test file + it("the searchTags in the store should be a Set object", () => { + let searchTags = store.state.gridSearch.searchTags; // this is a Set() + assert(searchTags instanceof Set, "searchTags wrong variable type, should be Set()"); + }) + + + describe("grid tags (tagClickFn: add_tag_to_grid_filter)", () => { + + let propsData = { + tags: Array.from(testTags), + tagClickFn: "add_tag_to_grid_filter", + id: "fakeID", + itemClass: "fakeItemClass" + }; + + beforeEach(function () { + wrapper = mount(StandardTags, { + store, + propsData + }); + }) + + afterEach(function() { + mockRedirect.reset(); + store.dispatch("clearSearchTags"); + }) + + it("clicking on a search tag should put that tag in the global store", () => { + clickFirstTag(); + let searchTags = store.state.gridSearch.searchTags; // this is a Set() + assert(searchTags.has(testTags[0]), "clicked tag not in store"); + assert(searchTags.size == 1, `wrong number of searchTags in store: ${searchTags.size}`); + }) + + it("clicking the same tag twice should add and remove it from the global store", () => { + clickFirstTag(); + clickFirstTag(); + let searchTags = store.state.gridSearch.searchTags; + assert(!searchTags.has(testTags[0]), "clicked tag shouldn't be in store"); + }) + + it("clicking any odd number of times should put the tag in the store", () => { + let oddNumber = 2 * Math.floor(Math.random() * 10) + 1; + for(let i=0; i < oddNumber; i++) { + clickFirstTag(); + } + let searchTags = store.state.gridSearch.searchTags; + assert(searchTags.has(testTags[0]), "clicked tag not in store"); + }) + + it("clicking any even number of times should remove the tag from the store", () => { + let evenNumber = 2 * Math.floor(Math.random() * 10); + for(let i=0; i < evenNumber; i++) { + clickFirstTag(); + } + let searchTags = store.state.gridSearch.searchTags; + assert(!searchTags.has(testTags[0]), "clicked tag shouldn't be in store"); + }) + + }) + + describe("community tag (tagClickFn: community_tag_click)", () => { + + let propsData = { + tags: ["abc", "def"], + tagClickFn: "community_tag_click", + clickUrl: "foo/bar", + id: "fakeID", + itemClass: "fakeItemClass" + }; + + beforeEach(function () { + wrapper = mount(StandardTags, { + store, + propsData + }); + }) + + afterEach(function() { + mockRedirect.reset(); + store.dispatch("clearSearchTags"); + }) + + // tagClickFn=community_tag_click, clickUrl=something + it("should try to redirect when you click a 'community tag'", () => { + clickFirstTag(); + let expectedUrl = `${propsData.clickUrl}?f-tags=${testTags[0]}`; + assert(mockRedirect.calledWith(expectedUrl), "requested wrong url"); + }) + + }) + +}) diff --git a/client/galaxy/scripts/components/Tags/StandardTags.vue b/client/galaxy/scripts/components/Tags/StandardTags.vue new file mode 100644 index 00000000000..d86116ac2c0 --- /dev/null +++ b/client/galaxy/scripts/components/Tags/StandardTags.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/client/galaxy/style/scss/autocomplete_tagging.scss b/client/galaxy/scripts/components/Tags/autocomplete_tagging.scss similarity index 100% rename from client/galaxy/style/scss/autocomplete_tagging.scss rename to client/galaxy/scripts/components/Tags/autocomplete_tagging.scss diff --git a/client/galaxy/scripts/components/Tags/index.js b/client/galaxy/scripts/components/Tags/index.js new file mode 100644 index 00000000000..effa89eae7b --- /dev/null +++ b/client/galaxy/scripts/components/Tags/index.js @@ -0,0 +1,3 @@ +export { default as GalaxyTags } from "./GalaxyTags"; +export { default as StandardTags } from "./StandardTags"; +export { mountTaggingComponent } from "./inits"; diff --git a/client/galaxy/scripts/components/Tags/inits.js b/client/galaxy/scripts/components/Tags/inits.js new file mode 100644 index 00000000000..4b2573b458b --- /dev/null +++ b/client/galaxy/scripts/components/Tags/inits.js @@ -0,0 +1,12 @@ +/** + * Initialization functions for tagging component. This is a bridge between the + * python-rendered page and an eventual component-based architecture. These + * functions pass in a set of python-rendered configuration variables and + * instantiate a component. In the near future, we'll just pass props to the + * component from the parent components and do away with this hybrid approach. + */ + +import StandardTags from "./StandardTags"; +import { mountVueComponent } from "utils/mountVueComponent"; + +export const mountTaggingComponent = mountVueComponent(StandardTags); diff --git a/client/galaxy/scripts/components/Tags/model.js b/client/galaxy/scripts/components/Tags/model.js new file mode 100644 index 00000000000..59f1054c2ec --- /dev/null +++ b/client/galaxy/scripts/components/Tags/model.js @@ -0,0 +1,46 @@ +/** + * The tag model is pretty simple, so this extra file might be overkill, but + * it's good practice to separate data modeling from data retrieval + */ + + +// model prototype + +function TagModel(props = {}) { + this.text = ""; + Object.assign(this, props); +} + +TagModel.prototype.equals = function(otherTag) { + return (this.text == otherTag.text); +} + +TagModel.prototype.toString = function() { + return this.text; +} + + +// Public factory + +export function createTag(data) { + let props = {}; + switch (typeof(data)) { + case "string": + props = { text: data }; + break; + case "object": + props = Object.assign({}, data); + break; + } + return new TagModel(props); +} + + + +// Returns tags in "newTags" that aren't present in "existingTags" + +export const diffTags = (newTags, existingTags) => { + let newModels = newTags.map(createTag); + let existingModels = existingTags.map(createTag); + return newModels.filter(tag => !existingModels.some(st => st.equals(tag))); +} diff --git a/client/galaxy/scripts/components/Tags/model.test.js b/client/galaxy/scripts/components/Tags/model.test.js new file mode 100644 index 00000000000..ce43b0d25c5 --- /dev/null +++ b/client/galaxy/scripts/components/Tags/model.test.js @@ -0,0 +1,60 @@ +import { createTag, diffTags } from "./model"; + + +describe("Tags/model.js", () => { + + + // Basic props + + describe("tag model", () => { + it("should have a string representation equal to text prop", () => { + let testLabel = "abc"; + let model = createTag(testLabel); + assert.equal(model, testLabel); + assert.equal(model.text, testLabel); + assert.equal(model.toString(), testLabel); + }) + }) + + + // Factory Function + + describe("createTag", () => { + + it("should build a model from a string", () => { + let label = "floob"; + let model = createTag(label); + expect(model.text).to.equal(label); + }) + + it("should build a model from an object", () => { + let data = { text: "floob" }; + let model = createTag(data); + expect(model.text).to.equal(data.text); + }) + }) + + + // Filtered select function, currently used in component to remove + // selected items from a list of returned autocomplete options + + describe("diffTags", () => { + + let source, selected; + + beforeEach(() => { + source = ["a", "b", "c", "d"].map(createTag); + selected = ["a", "d", "f"].map(createTag); + }) + + it("should remove duplicates from a passed array", () => { + let result = diffTags(source, selected); + expect(result.length).to.equal(2); + assert(result[0].equals(source[1]), true); + assert(result[0].equals(createTag("b")), true); + assert(result[1].equals(source[2]), true); + assert(result[1].equals(createTag("c")), true); + }) + }) + +}) diff --git a/client/galaxy/scripts/components/Tags/tagService.js b/client/galaxy/scripts/components/Tags/tagService.js new file mode 100644 index 00000000000..0b74a28ea0d --- /dev/null +++ b/client/galaxy/scripts/components/Tags/tagService.js @@ -0,0 +1,111 @@ +/** + * Generates a service object used by the tagging component to save, delete and + * lookup potential tag options for the autocomplete feature. Standard typeahead + * debouncing and ajax cancelling functionality is also provided here, though an + * argument can be made that it more properly belongs in the component that is + * handling the inputs. + * + * TODO: convert this python endpoint to a legit json REST service + */ + +import axios from "axios"; +import { createTag } from "./model"; +import { Subject } from "rxjs"; +import { filter, debounceTime, switchMap, distinctUntilChanged } from "rxjs/operators"; + + +export function buildTagService({ id, itemClass, context, debounceInterval = 150 }) { + + + /** + * Save tag, input can be text string or tag model + * @param {string|Tag} tag + * @returns Promise yielding new tag model + */ + async function saveTag(rawTag) { + let tag = createTag(rawTag); + let url = `/tag/add_tag_async?item_id=${id}&item_class=${itemClass}&context=${context}&new_tag=${tag.text}`; + let response = await axios.get(url); + if (response.status !== 200) { + throw new Error(`Unable to save tag: ${tag}`); + } + return createTag(tag); + } + + /** + * Delete tag, input can be text string or tag model + * @param {string|Tag} tag + * @returns Promise yielding deleted tag model + */ + async function deleteTag(rawTag) { + let tag = createTag(rawTag); + let url = `/tag/remove_tag_async?item_id=${id}&item_class=${itemClass}&context=${context}&tag_name=${tag.text}`; + let response = await axios.get(url); + if (response.status !== 200) { + throw new Error(`Unable to delete tag: ${tag}`); + } + return tag; + } + + /** + * Looks up autocomplete options based on search text + * @param {string} searchText + * @returns Promise yielding an array of tag models + */ + async function autocomplete(searchText) { + let url = `/tag/tag_autocomplete_data?item_id=${id}&item_class=${itemClass}&q=${searchText}`; + let response = await axios.get(url); + if (response.status !== 200) { + throw new Error(`Unable to retrieve autocomplete tags for search string: ${searchText}`); + } + return parseAutocompleteResults(response.data).map(createTag); + } + + /** + * Incoming autocomplete search text buffer + */ + const _searchText = new Subject(); + + return { + + // saves a single tag + save: saveTag, + + // deletes a single tag + delete: deleteTag, + + // returns options matching a search string for autocomplete this is + // exposed for testing purposes only, in practice a consuming component + // will set the autocompleteSearchText property and observe results by + // subscribing to the autocompleteOptions observable property + autocomplete, + + // input point for autocomplete text search + set autocompleteSearchText(txt) { + _searchText.next(txt); + }, + + // output of autocomplete search results + // subscribe to this observable to get results + autocompleteOptions: _searchText.pipe( + filter(txt => txt.length), + debounceTime(debounceInterval), + distinctUntilChanged(), + switchMap(autocomplete) + ) + } +} + + +/** + * Parser for the archaic result format in the current API. + * See testData/autocompleteResponse.txt for a sample + * @param {string} rawResponse + */ +export function parseAutocompleteResults(rawResponse) { + return rawResponse.split("\n") + .filter(line => line.includes("|")) + .map(line => line.split("|")[0]) + .filter(label => label.length) + .filter(label => label !== "#Header"); +} diff --git a/client/galaxy/scripts/components/Tags/tagService.test.js b/client/galaxy/scripts/components/Tags/tagService.test.js new file mode 100644 index 00000000000..3726cca0993 --- /dev/null +++ b/client/galaxy/scripts/components/Tags/tagService.test.js @@ -0,0 +1,152 @@ +import sinon from "sinon"; +import { buildTagService, __RewireAPI__ as rewire } from "./tagService"; +import { createTag } from "./model"; +import { interval } from "rxjs"; +import { take, takeUntil } from "rxjs/operators"; + +// test response +import autocompleteResponse from "./testData/autocompleteResponse.txt"; + + +describe("Tags/tagService.js", () => { + + let svcParams = { + id: 123, + itemClass: "fooClass", + context: "", + debounceInterval: 50 // shorter value than default for unit tests + }; + + let svc = buildTagService(svcParams); + + let mockAxios = { + get: () => null + }; + + let stub; + + beforeEach(() => { + rewire.__Rewire__("axios", mockAxios); + }) + + afterEach(() => { + if (stub) stub.restore(); + }) + + describe("save", () => { + + let testLabel = "fo0bar123"; + let testTag = createTag(testLabel); + + let { id, itemClass, context } = svcParams; + let expectedSaveUrl = `/tag/add_tag_async?item_id=${id}&item_class=${itemClass}&context=${context}&new_tag=${testLabel}` + + it("should save a string tag", async () => { + stub = sinon.stub(mockAxios, "get").resolves({ status: 200 }); + let savedTag = await svc.save(testLabel); + expect(savedTag.text).to.equal(testLabel); + assert(stub.calledWith(expectedSaveUrl)); + }) + + it("should save an object tag", async () => { + stub = sinon.stub(mockAxios, "get").resolves({ status: 200 }); + let savedTag = await svc.save(testTag); + expect(savedTag.text).to.equal(testLabel); + assert(stub.calledWith(expectedSaveUrl)); + }) + + // TODO: test error conditions + + }) + + describe("delete", () => { + + let testLabel = "fo0bar123"; + let testTag = createTag(testLabel); + + let { id, itemClass, context } = svcParams; + let expectedDeleteUrl = `/tag/remove_tag_async?item_id=${id}&item_class=${itemClass}&context=${context}&tag_name=${testLabel}`; + + it("should delete a text tag", async () => { + stub = sinon.stub(mockAxios, "get").resolves({ status: 200 }); + let result = await svc.delete(testTag); + assert(result); + assert(stub.calledWith(expectedDeleteUrl)); + }) + + it("should delete an object tag", async () => { + stub = sinon.stub(mockAxios, "get") + .resolves({ status: 200 }); + let result = await svc.delete(testTag); + assert(result); + assert(stub.calledWith(expectedDeleteUrl)); + }) + + // TODO: test error conditions + + }) + + describe("autocomplete", () => { + + let searchString = "foo"; + let { id, itemClass } = svcParams; + let expectedSearchUrl = `/tag/tag_autocomplete_data?item_id=${id}&item_class=${itemClass}&q=${searchString}`; + + let successResponse = { + status: 200, + data: autocompleteResponse + } + + let checkAutocompleteResult = (result) => { + assert(result); + assert(result instanceof Array); + assert(result.length == 2); + assert(result[0] instanceof Object); + assert(result[0].text = "abc"); + } + + // straight ajax request, unused in practice, but it's easier to + // test this call if we just expose it + it("ajax call should return tag objects", async () => { + stub = sinon.stub(mockAxios, "get").resolves(successResponse); + let result = await svc.autocomplete(searchString); + assert(stub.calledWith(expectedSearchUrl), "Called with wrong search url"); + checkAutocompleteResult(result); + }) + + // hit the search input with multiple entries, only one ajax call + // should result because of debouncing + it("should debounce autocomplete search inputs", (done) => { + + let searchResult; + + // ends subscription to observable so test doesn't go on forever + let spamCount = Math.floor(Math.random() * 10); + let timer = interval(1500).pipe(take(1)); + + // stub ajax request to return the success response if the + // searchString is the expected input + stub = sinon.stub(mockAxios, "get").resolves(successResponse); + + svc.autocompleteOptions + .pipe(takeUntil(timer)) + .subscribe( + result => searchResult = result, + err => console.log("error", err), + () => { + assert(stub.called, "Ajax call not made"); + assert(stub.callCount == 1, `Wrong number of ajax calls: ${stub.callCount}`); + checkAutocompleteResult(searchResult); + done(); + } + ); + + // spam a bunch of key inputs + for(var i=0; i < spamCount; i++) + svc.autocompleteSearchText = new String(Math.random()); + svc.autocompleteSearchText = searchString; + }) + + }) + +}) diff --git a/client/galaxy/scripts/components/Tags/tagStyles.scss b/client/galaxy/scripts/components/Tags/tagStyles.scss new file mode 100644 index 00000000000..2d7258140f5 --- /dev/null +++ b/client/galaxy/scripts/components/Tags/tagStyles.scss @@ -0,0 +1,74 @@ +// TODO: use general color definition file +@import "theme/blue.scss"; +@import "scss/mixins.scss"; + +// Puts a little graphic in place of the text-input +// when the input is not in focus +@mixin newTagHoverButton() { + .vue-tags-input .ti-tags .ti-new-tag-input-wrapper { + input { + background-color: transparent; + } + input:not(:focus) { + background: url("/static/images/fugue/tag--plus.png"); + background-repeat: no-repeat; + color: transparent; + &::placeholder { + color: transparent; + } + } + } +} + +// hides tag container +@mixin hideTagContainer() { + .vue-tags-input { + background-color: transparent; + .ti-input { + border: none; + } + } +} + +// general style butchering +@mixin matchBootstrapStyling() { + .vue-tags-input { + @include fill(); + .ti-tag { + border-radius: 4px; + font-size: 0.8rem; + font-weight: 400; + } + } +} + +// Version of the tags that only allow clicking +// existing tags instead of the full editing UI +@mixin forDisplayOnly() { + .vue-tags-input { + .ti-actions, + .ti-new-tag-input-wrapper { + display: none; + } + } +} + +.galaxy-tags { + + // adds in a graphic in place of the text input + @include newTagHoverButton(); + + // match bootstrap tag styles/colors + @include matchBootstrapStyling(); + + // display-only tags + &.disabled { + @include forDisplayOnly(); + } +} + +// Hide the border around the tag editing container +// when inside a grid row +.grid .galaxy-tags { + @include hideTagContainer(); +} diff --git a/client/galaxy/scripts/components/Tags/testData/autocompleteResponse.txt b/client/galaxy/scripts/components/Tags/testData/autocompleteResponse.txt new file mode 100644 index 00000000000..7d8b85f44af --- /dev/null +++ b/client/galaxy/scripts/components/Tags/testData/autocompleteResponse.txt @@ -0,0 +1,3 @@ +#Header|Your Tags +abc|abc +def|def \ No newline at end of file diff --git a/client/galaxy/scripts/entry/analysis/AnalysisRouter.js b/client/galaxy/scripts/entry/analysis/AnalysisRouter.js index 2d8b1eb123e..3fc93a5cb39 100644 --- a/client/galaxy/scripts/entry/analysis/AnalysisRouter.js +++ b/client/galaxy/scripts/entry/analysis/AnalysisRouter.js @@ -172,7 +172,8 @@ export const getAnalysisRouter = Galaxy => }, show_histories: function(action_id) { - this.page.display(new HistoryList.View({ action_id: action_id })); + let view = new HistoryList.View({ action_id: action_id }); + this.page.display(view); }, show_history_citations: function() { diff --git a/client/galaxy/scripts/mvc/grid/grid-view.js b/client/galaxy/scripts/mvc/grid/grid-view.js index 63f5cf0922b..88c32522068 100644 --- a/client/galaxy/scripts/mvc/grid/grid-view.js +++ b/client/galaxy/scripts/mvc/grid/grid-view.js @@ -8,6 +8,8 @@ import Templates from "mvc/grid/grid-template"; import PopupMenu from "mvc/ui/popup-menu"; import LoadingIndicator from "ui/loading-indicator"; import { init_refresh_on_change } from "onload/globalInits/init_refresh_on_change"; +import store from "../../store"; +import slug from "slug"; // This is necessary so that, when nested arrays are used in ajax/post/get methods, square brackets ('[]') are // not appended to the identifier of a nested array. @@ -20,22 +22,27 @@ export default Backbone.View.extend({ // Initialize initialize: function(grid_config) { + console.log("Grid initialize", grid_config, this); this.grid = new GridModel(); this.title = grid_config.title; this.active_tab = grid_config.active_tab; var self = this; - // Why is this a global? - window.add_tag_to_grid_filter = (tag_name, tag_value) => { - // Put tag name and value together. - var tag = tag_name + (tag_value !== undefined && tag_value !== "" ? `:${tag_value}` : ""); - var advanced_search = $("#advanced-search").is(":visible"); - if (!advanced_search) { - $("#standard-search").slideToggle("fast"); - $("#advanced-search").slideToggle("fast"); + // Subscribe to changes in the store, currently just storing + // tag changes from the tagging components, but that will change + // when we rework the grid. This subscription ties this older grid + // code to the new vue components + store.watch( + (state) => state.gridSearch.searchTags, + (newTags) => { + let tagArray = Array.from(newTags); + self.grid.add_filter("tags", tagArray, false); + self.openAdvancedSearch(); + self.render_filter_button("tags", tagArray); + self.go_page_one(); + self.execute(); } - self.add_filter_condition("tags", tag); - }; + ); if (grid_config.url_base && !grid_config.items) { LoadingIndicator.markViewAsLoading(this); @@ -66,6 +73,14 @@ export default Backbone.View.extend({ } }, + openAdvancedSearch: function() { + var isOpen = $("#advanced-search").is(":visible"); + if (!isOpen) { + $("#standard-search").slideToggle("fast"); + $("#advanced-search").slideToggle("fast"); + } + }, + // refresh frames handle_refresh: function(refresh_frames) { if (refresh_frames) { @@ -98,6 +113,9 @@ export default Backbone.View.extend({ // append main template this.$el.html(Templates.grid(options)); + + // add a class identifier for styling purposes + this.$el.addClass(this.getRootClassName(grid_config)); // update div contents this.$el.find("#grid-table-header").html(Templates.header(options)); @@ -310,6 +328,15 @@ export default Backbone.View.extend({ // Add condition to grid. this.grid.add_filter(name, value, true); + this.render_filter_button(name, value); + + // execute + this.go_page_one(); + this.execute(); + }, + + render_filter_button: function(name, value) { + // Add button that displays filter and provides a button to delete it. var t = $(Templates.filter_element(name, value)); var self = this; @@ -324,16 +351,17 @@ export default Backbone.View.extend({ // append to container var container = this.$el.find(`#${name}-filtering-criteria`); container.append(t); - - // execute - this.go_page_one(); - this.execute(); }, // Remove a condition to the grid filter; this adds the condition and refreshes the grid. - remove_filter_condition: function(name, value) { + remove_filter_condition: function(name, value) { // Remove filter condition. this.grid.remove_filter(name, value); + + // update vuex if the one criteria we're currently tracking changes + if (name == "tags") { + store.dispatch("removeSearchTag", { text: value }); + } // Execute this.go_page_one(); @@ -665,5 +693,13 @@ export default Backbone.View.extend({ }); } }); + }, + + // Generates a class name at the root of the view that we can + // use for conditional styling in the various kinds of grids + // instead of acres of if/then statements in javascript + getRootClassName({ title = "grid" }) { + return slug(title).toLowerCase(); } + }); diff --git a/client/galaxy/scripts/sample.test.js b/client/galaxy/scripts/sample.test.js deleted file mode 100644 index de7697b1a7b..00000000000 --- a/client/galaxy/scripts/sample.test.js +++ /dev/null @@ -1,12 +0,0 @@ -describe("Sample Test", () => { - it("hey look, tests run", () => { - assert.equal(1, 1); - }); - - it("prove I can use a Proxy", () => { - let target = {}; - let thing = new Proxy(target, {}); - thing.foo = 232; - assert.equal(target.foo, thing.foo); - }); -}); diff --git a/client/galaxy/scripts/store/gridSearchStore.js b/client/galaxy/scripts/store/gridSearchStore.js new file mode 100644 index 00000000000..44ea51fe266 --- /dev/null +++ b/client/galaxy/scripts/store/gridSearchStore.js @@ -0,0 +1,33 @@ +/** + * Vuex store module used for managing search parameters in the grid. Currently + * only manages the tags that access this store via the new tagging components, + * but presumably the entire grid search filter criteria will live here one day. + */ + +export const gridSearchStore = { + state: { + searchTags: new Set() + }, + mutations: { + // TODO: we could write an equivalence comparator here for searchTag + // Sets and not register a change if the new set is equivalent + setSearchTags(state, tags) { + state.searchTags = new Set(tags); + } + }, + actions: { + toggleSearchTag({ state, commit }, { text }) { + let tags = new Set(state.searchTags); + tags.has(text) ? tags.delete(text) : tags.add(text); + commit("setSearchTags", tags); + }, + removeSearchTag({ state, commit }, { text }) { + let tags = new Set(state.searchTags); + tags.delete(text); + commit("setSearchTags", tags); + }, + clearSearchTags({ state, commit }) { + commit("setSearchTags", new Set()); + } + } +} diff --git a/client/galaxy/scripts/store/index.js b/client/galaxy/scripts/store/index.js new file mode 100644 index 00000000000..14a1e97254a --- /dev/null +++ b/client/galaxy/scripts/store/index.js @@ -0,0 +1,17 @@ +/** + * Central Vuex store + */ + +import Vue from "vue"; +import Vuex from "vuex"; + +// initial use of central store for search parameter housing. Test was to see if +// we could set store values from a component (CommunityTags.vue) and observe +// changes in legacy code (see grid-view.js) +import { gridSearchStore as gridSearch } from "./gridSearchStore"; + +Vue.use(Vuex); + +export default new Vuex.Store({ + modules: { gridSearch } +}); diff --git a/client/galaxy/scripts/ui/autocom_tagging.js b/client/galaxy/scripts/ui/autocom_tagging.js deleted file mode 100644 index 6420484002b..00000000000 --- a/client/galaxy/scripts/ui/autocom_tagging.js +++ /dev/null @@ -1,369 +0,0 @@ -import $ from "jquery"; -import _ from "underscore"; - -// ============================================================================ -/** - * JQuery extension for tagging with autocomplete. - * @author: Jeremy Goecks - * @require: jquery.autocomplete plugin - */ -// -// Initialize "tag click functions" for tags. -// -export function init_tag_click_function(tag_elt, click_func) { - $(tag_elt) - .find(".tag-name") - .each(function() { - $(this).click(function() { - var tag_str = $(this).text(); - var tag_name_and_value = tag_str.split(":"); - click_func(tag_name_and_value[0], tag_name_and_value[1]); - return true; - }); - }); -} - -$.fn.autocomplete_tagging = function(options) { - var defaults = { - get_toggle_link_text_fn: function(tags) { - var text = ""; - var num_tags = _.size(tags); - if (num_tags > 0) { - text = num_tags + (num_tags > 1 ? " Tags" : " Tag"); - } else { - text = "Add tags"; - } - return text; - }, - tag_click_fn: function(name, value) {}, - editable: true, - input_size: 20, - in_form: false, - tags: {}, - use_toggle_link: true, - item_id: "", - add_tag_img: "", - add_tag_img_rollover: "", - delete_tag_img: "", - ajax_autocomplete_tag_url: "", - ajax_retag_url: "", - ajax_delete_tag_url: "", - ajax_add_tag_url: "" - }; - - var settings = $.extend(defaults, options); - - // - // Initalize object's elements. - // - - // Get elements for this object. For this_obj, assume the last element with the id is the "this"; this is somewhat of a hack to address the problem - // that there may be two tagging elements for a single item if there are both community and individual tags for an element. - var this_obj = $(this); - var tag_area = this_obj.find(".tag-area"); - var toggle_link = this_obj.find(".toggle-link"); - var tag_input_field = this_obj.find(".tag-input"); - var add_tag_button = this_obj.find(".add-tag-button"); - - // Initialize toggle link. - toggle_link.click(function() { - // Take special actions depending on whether toggle is showing or hiding link. - var after_toggle_fn; - if (tag_area.is(":hidden")) { - after_toggle_fn = function() { - // If there are no tags, go right to editing mode by generating a click on the area. - var num_tags = $(this).find(".tag-button").length; - if (num_tags === 0) { - tag_area.click(); - } - }; - } else { - after_toggle_fn = () => { - tag_area.blur(); - }; - } - tag_area.slideToggle("fast", after_toggle_fn); - return $(this); - }); - - // Initialize tag input field. - if (settings.editable) { - tag_input_field.hide(); - } - tag_input_field.keyup(function(e) { - if (e.keyCode === 27) { - // Escape key - $(this).trigger("blur"); - } else if ( - e.keyCode === 13 || // Return Key - e.keyCode === 188 || // Comma - e.keyCode === 32 // Space - ) { - // - // Check input. - // - - var new_value = this.value; - - // Suppress space after a ":" - if (new_value.indexOf(": ", new_value.length - 2) !== -1) { - this.value = new_value.substring(0, new_value.length - 1); - return false; - } - - // Remove trigger keys from input. - if (e.keyCode === 188 || e.keyCode === 32) { - new_value = new_value.substring(0, new_value.length - 1); - } - - // Trim whitespace. - new_value = $.trim(new_value); - - // Too short? - if (new_value.length < 2) { - return false; - } - - // - // New tag OK - apply it. - // - - this.value = ""; // Reset text field now that tag is being added - - // Add button for tag after all other tag buttons. - var new_tag_button = build_tag_button(new_value); - var tag_buttons = tag_area.children(".tag-button"); - if (tag_buttons.length !== 0) { - var last_tag_button = tag_buttons.slice(tag_buttons.length - 1); - last_tag_button.after(new_tag_button); - } else { - tag_area.prepend(new_tag_button); - } - - // Add tag to internal list. - var tag_name_and_value = new_value.split(":"); - settings.tags[tag_name_and_value[0]] = tag_name_and_value[1]; - - // Update toggle link text. - var new_text = settings.get_toggle_link_text_fn(settings.tags); - toggle_link.text(new_text); - - // Commit tag to server. - var zz = $(this); - $.ajax({ - url: settings.ajax_add_tag_url, - data: { new_tag: new_value }, - error: function() { - // Failed. Roll back changes and show alert. - new_tag_button.remove(); - delete settings.tags[tag_name_and_value[0]]; - var new_text = settings.get_toggle_link_text_fn(settings.tags); - toggle_link.text(new_text); - alert("Add tag failed"); - }, - success: function() { - // Flush autocomplete cache because it's not out of date. - // TODO: in the future, we could remove the particular item - // that was chosen from the cache rather than flush it. - zz.data("autocompleter").cacheFlush(); - } - }); - - return false; - } - }); - - // Add autocomplete to input. - var format_item_func = (key, row_position, num_rows, value, search_term) => { - var tag_name_and_value = value.split(":"); - return tag_name_and_value.length === 1 ? tag_name_and_value[0] : tag_name_and_value[1]; - }; - var autocomplete_options = { - selectFirst: false, - formatItem: format_item_func, - autoFill: false, - highlight: false - }; - tag_input_field.autocomplete_verheul(settings.ajax_autocomplete_tag_url, autocomplete_options); - - // Initialize delete tag images for current tags. - this_obj.find(".delete-tag-img").each(function() { - init_delete_tag_image($(this)); - }); - - // Initialize tag click function. - init_tag_click_function($(this), settings.tag_click_fn); - - // Initialize "add tag" button. - add_tag_button.click(function() { - $(this).hide(); - - // Clicking on button is the same as clicking on the tag area. - tag_area.click(); - return false; - }); - - // - // Set up tag area interactions; these are needed only if tags are editable. - // - if (settings.editable) { - // When the tag area blurs, go to "view tag" mode. - tag_area.bind("blur", e => { - if (_.size(settings.tags) > 0) { - add_tag_button.show(); - tag_input_field.hide(); - tag_area.removeClass("active-tag-area"); - // tag_area.addClass("tooltip"); - } else { - // No tags, so do nothing to ensure that input is still visible. - } - }); - - // On click, enable user to add tags. - tag_area.click(function(e) { - var is_active = $(this).hasClass("active-tag-area"); - - // If a "delete image" object was pressed and area is inactive, do nothing. - if ($(e.target).hasClass("delete-tag-img") && !is_active) { - return false; - } - - // If a "tag name" object was pressed and area is inactive, do nothing. - if ($(e.target).hasClass("tag-name") && !is_active) { - return false; - } - - // Remove tooltip. - // $(this).removeClass("tooltip"); - - // Hide add tag button, show tag_input field. Change background to show - // area is active. - $(this).addClass("active-tag-area"); - add_tag_button.hide(); - tag_input_field.show(); - tag_input_field.focus(); - - // Add handler to document that will call blur when the tag area is blurred; - // a tag area is blurred when a user clicks on an element outside the area. - var handle_document_click = e => { - var check_click = function(tag_area, target) { - // Blur the tag area if the element clicked on is not in the tag area. - if (target !== tag_area) { - tag_area.blur(); - $(window).unbind("click.tagging_blur"); - $(this).addClass("tooltip"); - } - }; - check_click(tag_area, $(e.target)); - }; - // TODO: we should attach the click handler to all frames in order to capture - // clicks outside the frame that this element is in. - //window.parent.document.onclick = handle_document_click; - //var temp = $(window.parent.document.body).contents().find("iframe").html(); - //alert(temp); - //$(document).parent().click(handle_document_click); - $(window).bind("click.tagging_blur", handle_document_click); - - return false; - }); - } - - // If using toggle link, hide the tag area. Otherwise, show the tag area. - if (settings.use_toggle_link) { - tag_area.hide(); - } - - // - // Helper functions. - // - - // Initialize a "delete tag image": when click, delete tag from UI and send delete request to server. - function init_delete_tag_image(delete_img) { - $(delete_img).mouseenter(function() { - $(this).attr("src", settings.delete_tag_img_rollover); - }); - $(delete_img).mouseleave(function() { - $(this).attr("src", settings.delete_tag_img); - }); - $(delete_img).click(function() { - // Tag button is image's parent. - var tag_button = $(this).parent(); - - // Get tag name, value. - var tag_name_elt = tag_button.find(".tag-name").eq(0); - var tag_str = tag_name_elt.text(); - var tag_name_and_value = tag_str.split(":"); - var tag_name = tag_name_and_value[0]; - var tag_value = tag_name_and_value[1]; - - var prev_button = tag_button.prev(); - tag_button.remove(); - - // Remove tag from local list for consistency. - delete settings.tags[tag_name]; - - // Update toggle link text. - var new_text = settings.get_toggle_link_text_fn(settings.tags); - toggle_link.text(new_text); - - // Delete tag. - $.ajax({ - url: settings.ajax_delete_tag_url, - data: { tag_name: tag_name }, - error: function() { - // Failed. Roll back changes and show alert. - settings.tags[tag_name] = tag_value; - if (prev_button.hasClass("tag-button")) { - prev_button.after(tag_button); - } else { - tag_area.prepend(tag_button); - } - alert("Remove tag failed"); - - toggle_link.text(settings.get_toggle_link_text_fn(settings.tags)); - - // TODO: no idea why it's necessary to set this up again. - delete_img.mouseenter(function() { - $(this).attr("src", settings.delete_tag_img_rollover); - }); - delete_img.mouseleave(function() { - $(this).attr("src", settings.delete_tag_img); - }); - }, - success: function() {} - }); - - return true; - }); - } - - // - // Function that builds a tag button. - // - function build_tag_button(tag_str) { - // Build "delete tag" image. - var delete_img = $("") - .attr("src", settings.delete_tag_img) - .addClass("delete-tag-img"); - init_delete_tag_image(delete_img); - - // Build tag button. - var tag_name_elt = $("") - .text(tag_str) - .addClass("tag-name"); - tag_name_elt.click(() => { - var tag_name_and_value = tag_str.split(":"); - settings.tag_click_fn(tag_name_and_value[0], tag_name_and_value[1]); - return true; - }); - - var tag_button = $("").addClass("tag-button"); - tag_button.append(tag_name_elt); - // Allow delete only if element is editable. - if (settings.editable) { - tag_button.append(delete_img); - } - - return tag_button; - } -}; diff --git a/client/galaxy/scripts/unitTestBundle.js b/client/galaxy/scripts/unitTestBundle.js deleted file mode 100644 index 7de155c4784..00000000000 --- a/client/galaxy/scripts/unitTestBundle.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * A combination of all the mocha test files so that we can run them as a unit, - * which is much faster during deployment. - * - * Note, this is non-intuitive, but the parameters of require.context must be - * literals, can't even be stored in local variables or webpack can't do static - * dependency analysis. - * - * https://webpack.js.org/guides/dependency-management/ - */ -// eslint-disable-next-line no-undef -let testContext = require.context("./", true, /\.test\.js$/); -testContext.keys().forEach(testContext); diff --git a/client/galaxy/scripts/utils/mock.js b/client/galaxy/scripts/utils/mock.js index 571b511c18c..6761196706c 100644 --- a/client/galaxy/scripts/utils/mock.js +++ b/client/galaxy/scripts/utils/mock.js @@ -1,4 +1,4 @@ -// console.log api but does nothing +// Applies noop to all methods for mock creation const doNothing = () => null; diff --git a/client/galaxy/scripts/utils/mountVueComponent.js b/client/galaxy/scripts/utils/mountVueComponent.js new file mode 100644 index 00000000000..020645a70bb --- /dev/null +++ b/client/galaxy/scripts/utils/mountVueComponent.js @@ -0,0 +1,11 @@ +// Generic Vue component mount for use in transitional +// mount functions + +import Vue from "vue"; +import store from "../store"; + +export const mountVueComponent = (ComponentDefinition) => (propsData, el) => { + // console.log("mount function", propsData); + let component = Vue.extend(ComponentDefinition); + return new component({ store, propsData, el }); +} diff --git a/client/galaxy/scripts/utils/redirect.js b/client/galaxy/scripts/utils/redirect.js new file mode 100644 index 00000000000..add91b9cc3a --- /dev/null +++ b/client/galaxy/scripts/utils/redirect.js @@ -0,0 +1,5 @@ +// This file exists purely to make unit testing easier + +export function redirectToUrl(url) { + window.location = url; +} \ No newline at end of file diff --git a/client/galaxy/scripts/viz/trackster.js b/client/galaxy/scripts/viz/trackster.js index 9ab6dc122a3..01cdc27c62f 100644 --- a/client/galaxy/scripts/viz/trackster.js +++ b/client/galaxy/scripts/viz/trackster.js @@ -24,7 +24,6 @@ import "libs/jquery/jquery.form"; import "libs/jquery/jquery.rating"; import "ui/editable-text"; -//import "style/scss/autocomplete_tagging.scss"; //import "static/style/jquery-ui/smoothness/jquery-ui.css"; //import "static/style/library.css"; //import "static/style/trackster.css"; diff --git a/client/galaxy/style/scss/base.scss b/client/galaxy/style/scss/base.scss index bdc70014e91..f56d1ab5621 100644 --- a/client/galaxy/style/scss/base.scss +++ b/client/galaxy/style/scss/base.scss @@ -30,7 +30,6 @@ $fa-font-path: "../../../node_modules/font-awesome/fonts/"; @import "ui.scss"; @import "library.scss"; @import "trackster.scss"; -@import "autocomplete_tagging.scss"; @import "toastr.scss"; @import "jstree.scss"; @import "tour.scss"; diff --git a/client/galaxy/style/scss/mixins.scss b/client/galaxy/style/scss/mixins.scss new file mode 100644 index 00000000000..4e5685d4fc5 --- /dev/null +++ b/client/galaxy/style/scss/mixins.scss @@ -0,0 +1,8 @@ +// Utility mixin expands to container edges +@mixin fill() { + position: relative; + top: 0; + left: 0; + width: 100%; + height: 100%; +} diff --git a/client/karma/karma.config.mocha.js b/client/karma/karma.config.mocha.js index 058e1587095..122537c32db 100644 --- a/client/karma/karma.config.mocha.js +++ b/client/karma/karma.config.mocha.js @@ -4,38 +4,20 @@ const baseKarmaConfig = require("./karma.config.base"); -const single_pack = (process.env.GALAXY_TEST_AS_SINGLE_PACK == "true"); - -const testBundles = [ - "**/unitTestBundle.js", - "**/mocha/test.js" -]; - -const separateTests = [ - "**/*.test.js", - "**/mocha/tests/*_tests.js" -]; - module.exports = function (config) { - console.log("single_pack?", single_pack); - - // pick all separate tests or the dynamic test-bundles - let files = single_pack - ? testBundles - : separateTests; - - let preprocessors = files.reduce((result, path) => { - result[path] = ["webpack"]; - return result; - }, {}); - let settings = Object.assign({}, baseKarmaConfig, { - files: files, + files: [ + "../../node_modules/@babel/polyfill/dist/polyfill.js", + { pattern: "**/*.test.js", watched: true }, + // { pattern: "**/mocha/tests/*_tests.js" } + ], + preprocessors: { + "**/*.js": ["webpack"] + }, exclude: ["**/qunit/*"], - preprocessors: preprocessors, reporters: ["mocha"], - frameworks: ["polyfill", "mocha", "chai"] + frameworks: ["mocha", "chai"] }); config.set(settings); diff --git a/client/karma/karma.config.qunit.js b/client/karma/karma.config.qunit.js index 743b0eb86c7..54e585a4dc2 100644 --- a/client/karma/karma.config.qunit.js +++ b/client/karma/karma.config.qunit.js @@ -29,7 +29,8 @@ module.exports = function (config) { let settings = Object.assign({}, baseKarmaConfig, { files: testFiles.concat(assets), preprocessors: preprocessors, - frameworks: ["polyfill", "qunit"] + frameworks: ["polyfill", "qunit"], + singleRun: true }); config.set(settings); diff --git a/client/karma/webpack.config.unittest.js b/client/karma/webpack.config.unittest.js index 77483b5b2a3..c3d98dfec4f 100644 --- a/client/karma/webpack.config.unittest.js +++ b/client/karma/webpack.config.unittest.js @@ -3,28 +3,45 @@ * the ignore-loader for speedier testing. */ -let merge = require("webpack-merge"); -let wpConfig = require("../webpack.config"); +const merge = require("webpack-merge"); +const wpConfig = require("../webpack.config"); wpConfig.mode = "development"; wpConfig.entry = () => ({}); -// Don't need assets for unit testing, override those rules -module.exports = merge.smart(wpConfig, { - module: { - rules: [ - { - test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)(\?.*$|$)/, - loader: "ignore-loader" - }, - { - test: /\.css$/, - loader: "ignore-loader" - }, - { - test: /\.scss$/, - loader: "ignore-loader" - } - ] + +// Don't need any assets for unit testing + +let ignoreAssetLoaders = { + rules:[ + { + test: /\.(png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)(\?.*$|$)/, + loader: "ignore-loader" + }, + { + test: /\.css$/, + loader: "ignore-loader" + }, + { + test: /\.scss$/, + loader: "ignore-loader" + } + ] +}; + +wpConfig.module = merge.smart(wpConfig.module, ignoreAssetLoaders); + + +// Using babel-plugin-rewire to handle dependency mocking since webpack 4 +// exports immutable bindings for ES modules but we still need a way to +// overwrite dependencies during unit-testing. + +wpConfig.module.rules = wpConfig.module.rules.map(rule => { + if (rule.loader == "babel-loader") { + rule.options.plugins.push("rewire"); } + return rule; }); + + +module.exports = wpConfig; diff --git a/client/package.json b/client/package.json index cae03da10ba..8061b9dfb62 100644 --- a/client/package.json +++ b/client/package.json @@ -16,6 +16,8 @@ "@babel/polyfill": "^7.0.0", "@babel/preset-env": "^7.1.0", "@handsontable/vue": "^2.0.0-beta1", + "@johmun/vue-tags-input": "^2.0.0", + "@vue/test-utils": "^1.0.0-beta.28", "amdi18n-loader": "^0.8.0", "axios": "^0.18.0", "backbone": "1.3", @@ -44,9 +46,12 @@ "raven-js": "^3.27.0", "requirejs": "2.3.6", "rxjs": "^6.3.3", + "slug": "^0.9.3", "underscore": "^1.9.1", - "vue": "2.5.17", - "vue-router": "3.0.1" + "vue": "^2.5.22", + "vue-router": "^3.0.2", + "vue-rx": "^6.1.0", + "vuex": "^3.1.0" }, "scripts": { "watch": "gulp staging && gulp clean && gulp && yarn run save-build-hash && yarn run webpack-watch", @@ -69,13 +74,14 @@ "styleguide:build": "vue-styleguidist build", "test": "npm run test-mocha && npm run test-qunit", "test-watch": "npm run test-mocha -- --no-single-run", - "test-qunit": "GALAXY_TEST_AS_SINGLE_PACK=true karma start karma/karma.config.qunit.js", - "test-mocha": "GALAXY_TEST_AS_SINGLE_PACK=true karma start karma/karma.config.mocha.js", + "test-qunit": "karma start karma/karma.config.qunit.js", + "test-mocha": "karma start karma/karma.config.mocha.js", "jshint": "jshint --exclude='galaxy/scripts/libs/**' galaxy/scripts/**/*.js", "eslint": "eslint -c .eslintrc.js galaxy/scripts --ext .js,.vue" }, "devDependencies": { "babel-loader": "^8.0.4", + "babel-plugin-rewire": "^1.2.0", "babel-plugin-transform-inline-environment-variables": "^0.4.3", "babel-plugin-transform-vue-template": "^0.4.2", "chai": "^4.2.0", @@ -112,14 +118,14 @@ "phantomjs-prebuilt": "^2.1.7", "prettier": "^1.15.3", "qunitjs": "^2.4.1", + "raw-loader": "^1.0.0", "sass-loader": "^7.1.0", "sinon": "^4.1.2", "store": "^2.0.12", "style-loader": "^0.23.1", - "vue-loader": "^15.4.2", - "vue-style-loader": "^4.1.2", + "vue-loader": "^15.6.2", "vue-styleguidist": "^1.8.9", - "vue-template-compiler": "2.5.17", + "vue-template-compiler": "^2.5.22", "webpack": "^4.23.0", "webpack-cli": "^3.1.2", "webpack-merge": "^4.1.4", diff --git a/client/webpack.config.js b/client/webpack.config.js index 88d306b6e06..1c308fc40be 100644 --- a/client/webpack.config.js +++ b/client/webpack.config.js @@ -29,7 +29,8 @@ let buildconfig = { alias: { jquery$: `${libsBase}/jquery.custom.js`, jqueryVendor$: `${libsBase}/jquery/jquery.js`, - store$: "store/dist/store.modern.js" + store$: "store/dist/store.modern.js", + vue$: "vue/dist/vue.esm.js" } }, optimization: { @@ -64,7 +65,22 @@ let buildconfig = { libsBase ], loader: "babel-loader", - options: { babelrc: true } + options: { + cacheDirectory: true, + cacheCompression: false, + presets: [ + ["@babel/preset-env", { modules: false }] + ], + plugins: [ + "transform-vue-template", + "@babel/plugin-syntax-dynamic-import" + ], + ignore: [ + "i18n.js", + "utils/localization.js", + "nls/*" + ] + } }, { test: `${libsBase}/jquery.custom.js`, @@ -144,12 +160,15 @@ let buildconfig = { }, { loader: "sass-loader", - options: { sourceMap: true } + options: { + sourceMap: true, + includePaths: ["galaxy/style/scss"] + } } ] }, { - test: /\.tmpl$/, + test: /\.(txt|tmpl)$/, loader: "raw-loader" } ] diff --git a/client/yarn.lock b/client/yarn.lock index 23886d562b7..a810dae625a 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -754,6 +754,13 @@ resolved "https://registry.yarnpkg.com/@handsontable/vue/-/vue-2.0.0.tgz#28ae7d247a89738088abc32584562925dde18db0" integrity sha512-QVqJywrQWwJzoDCXibZkXrg+T91hNCTCN9qznmum4FSewL0MUtaHotv7Zc/D4scvEwpwWnKx5vpw4CY14V4OYw== +"@johmun/vue-tags-input@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@johmun/vue-tags-input/-/vue-tags-input-2.0.0.tgz#95870006ddd1320eb4979a58cec8ed24ee966c9b" + integrity sha512-SBNZm9ZtgwfnjEYUOkJ8Al7ZzUluZ3F49mO8EOTjTdYfB+ZJIQbf0R71PGYfKWkU7qdAwzW2imzlTPnJC3Wh7g== + dependencies: + vue "^2.5.16" + "@sinonjs/formatio@3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.0.0.tgz#9d282d81030a03a03fa0c5ce31fd8786a4da311a" @@ -802,6 +809,29 @@ source-map "^0.5.6" vue-template-es2015-compiler "^1.6.0" +"@vue/component-compiler-utils@^2.5.1": + version "2.5.2" + resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-2.5.2.tgz#a8d57e773354ab10e4742c7d6a8dd86184d4d7be" + integrity sha512-3exq9O89GXo9E+CGKzgURCbasG15FtFMs8QRrCUVWGaKue4Egpw41MHb3Avtikv1VykKfBq3FvAnf9Nx3sdVJg== + dependencies: + consolidate "^0.15.1" + hash-sum "^1.0.2" + lru-cache "^4.1.2" + merge-source-map "^1.1.0" + postcss "^7.0.14" + postcss-selector-parser "^5.0.0" + prettier "1.16.3" + source-map "~0.6.1" + vue-template-es2015-compiler "^1.8.2" + +"@vue/test-utils@^1.0.0-beta.28": + version "1.0.0-beta.28" + resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.0.0-beta.28.tgz#767c43413df8cde86128735e58923803e444b9a5" + integrity sha512-uVbFJG0g/H9hf2pgWUdhvQYItRGzQ44cMFf00wp0YEo85pxuvM9e3mx8QLQfx6R2CogxbK4CvV7qvkLblehXeQ== + dependencies: + dom-event-types "^1.0.0" + lodash "^4.17.4" + "@vxna/mini-html-webpack-template@^0.1.7": version "0.1.7" resolved "https://registry.yarnpkg.com/@vxna/mini-html-webpack-template/-/mini-html-webpack-template-0.1.7.tgz#2a8270e513ee14f395cc17c2ce22ced383c45d22" @@ -1839,6 +1869,11 @@ babel-plugin-espower@^2.1.0: espurify "^1.6.0" estraverse "^4.1.1" +babel-plugin-rewire@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/babel-plugin-rewire/-/babel-plugin-rewire-1.2.0.tgz#822562d72ed2c84e47c0f95ee232c920853e9d89" + integrity sha512-JBZxczHw3tScS+djy6JPLMjblchGhLI89ep15H3SyjujIzlxo5nr6Yjo7AXotdeVczeBmWs0tF8PgJWDdgzAkQ== + babel-plugin-syntax-async-functions@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" @@ -2968,6 +3003,15 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.4 escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + character-entities-html4@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.2.tgz#c44fdde3ce66b52e8d321d6c1bf46101f0150610" @@ -3927,6 +3971,11 @@ cssesc@^1.0.1: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-1.0.1.tgz#ef7bd8d0229ed6a3a7051ff7771265fe7330e0a8" integrity sha512-S2hzrpWvE6G/rW7i7IxJfWBYn27QWfOIncUW++8Rbo1VB5zsJDSVPcnI+Q8z7rhxT6/yZeLOCja4cZnghJrNGA== +cssesc@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703" + integrity sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg== + cssnano-preset-default@^4.0.2: version "4.0.3" resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.3.tgz#9bfd1b06d4aa3991ed958ad9b9ec25a179261705" @@ -4411,6 +4460,11 @@ dom-converter@~0.2: dependencies: utila "~0.4" +dom-event-types@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dom-event-types/-/dom-event-types-1.0.0.tgz#5830a0a29e1bf837fe50a70cd80a597232813cae" + integrity sha512-2G2Vwi2zXTHBGqXHsJ4+ak/iP0N8Ar+G8a7LiD2oup5o4sQWytwqqrZu/O6hIMV0KMID2PL69OhpshLO0n7UJQ== + dom-serialize@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" @@ -10243,6 +10297,15 @@ postcss-selector-parser@^3.0.0, postcss-selector-parser@^3.1.1: indexes-of "^1.0.1" uniq "^1.0.1" +postcss-selector-parser@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c" + integrity sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ== + dependencies: + cssesc "^2.0.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + postcss-selector-parser@^5.0.0-rc.3: version "5.0.0-rc.3" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0-rc.3.tgz#c4525dcc8eb90166c53dcbf0cb9317ceff5a15b5" @@ -10333,6 +10396,15 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.2: source-map "^0.6.1" supports-color "^5.5.0" +postcss@^7.0.14: + version "7.0.14" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.14.tgz#4527ed6b1ca0d82c53ce5ec1a2041c2346bbd6e5" + integrity sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + power-assert-context-formatter@^1.0.7: version "1.2.0" resolved "https://registry.yarnpkg.com/power-assert-context-formatter/-/power-assert-context-formatter-1.2.0.tgz#8fbe72692288ec5a7203cdf215c8b838a6061d2a" @@ -10467,6 +10539,11 @@ prettier@1.13.7: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.13.7.tgz#850f3b8af784a49a6ea2d2eaa7ed1428a34b7281" integrity sha512-KIU72UmYPGk4MujZGYMFwinB7lOf2LsDNGSOC8ufevsrPLISrZbNJlWstRi3m0AMuszbH+EFSQ/r6w56RSPK6w== +prettier@1.16.3: + version "1.16.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.3.tgz#8c62168453badef702f34b45b6ee899574a6a65d" + integrity sha512-kn/GU6SMRYPxUakNXhpP0EedT/KmaPzr0H5lIsDogrykbaxOpOfAFfk5XA7DZrJyMAv1wlMV3CPcZruGXVVUZw== + prettier@^1.15.3: version "1.15.3" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.15.3.tgz#1feaac5bdd181237b54dbe65d874e02a1472786a" @@ -10765,6 +10842,14 @@ raw-body@2.3.3: iconv-lite "0.4.23" unpipe "1.0.0" +raw-loader@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-1.0.0.tgz#3f9889e73dadbda9a424bce79809b4133ad46405" + integrity sha512-Uqy5AqELpytJTRxYT4fhltcKPj0TyaEpzJDcGz7DFJi+pQOOi3GjR/DOdxTkTsF+NzhnldIoG6TORaBlInUuqA== + dependencies: + loader-utils "^1.1.0" + schema-utils "^1.0.0" + rc@^1.0.1, rc@^1.1.6, rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -11889,6 +11974,13 @@ slide@^1.1.5: resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc= +slug@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/slug/-/slug-0.9.3.tgz#8c9c773d79367c0188733316cf49fd2b8db40f6a" + integrity sha512-DddSQQnUdAofjFOKRT+zsMNrdzdte04G5DUA+NeaUJlPAqR1bWQ22qVfayRxHFiRGR3bUV0wt5VSj4849pGKSw== + dependencies: + unicode ">= 0.3.1" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -12466,6 +12558,13 @@ supports-color@^5.1.0, supports-color@^5.3.0, supports-color@^5.4.0, supports-co dependencies: has-flag "^3.0.0" +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + svgo@^0.7.0: version "0.7.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" @@ -12960,6 +13059,11 @@ unicode-property-aliases-ecmascript@^1.0.4: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz#5a533f31b4317ea76f17d807fa0d116546111dd0" integrity sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg== +"unicode@>= 0.3.1": + version "11.0.1" + resolved "https://registry.yarnpkg.com/unicode/-/unicode-11.0.1.tgz#735bd422ec75cf28d396eb224d535d168d5f1db6" + integrity sha512-+cHtykLb+eF1yrSLWTwcYBrqJkTfX7Quoyg7Juhe6uylF43ZbMdxMuSHNYlnyLT8T7POAvavgBthzUF9AIaQvQ== + unified@^6.0.0: version "6.2.0" resolved "https://registry.yarnpkg.com/unified/-/unified-6.2.0.tgz#7fbd630f719126d67d40c644b7e3f617035f6dba" @@ -13377,7 +13481,7 @@ vue-hot-reload-api@^2.3.0: resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.1.tgz#b2d3d95402a811602380783ea4f566eb875569a2" integrity sha512-AA86yKZ5uOKz87/q1UpngEXhbRkaYg1b7HMMVRobNV1IVKqZe8oLIzo6iMocVwZXnYitlGwf2k4ZRLOZlS8oPQ== -vue-loader@^15.2.4, vue-loader@^15.4.2: +vue-loader@^15.2.4: version "15.4.2" resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.4.2.tgz#812bb26e447dd3b84c485eb634190d914ce125e2" integrity sha512-nVV27GNIA9MeoD8yQ3dkUzwlAaAsWeYSWZHsu/K04KCD339lW0Jv2sJWsjj3721SP7sl2lYdPmjcHgkWQSp5bg== @@ -13388,10 +13492,26 @@ vue-loader@^15.2.4, vue-loader@^15.4.2: vue-hot-reload-api "^2.3.0" vue-style-loader "^4.1.0" -vue-router@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.1.tgz#d9b05ad9c7420ba0f626d6500d693e60092cc1e9" - integrity sha512-vLLoY452L+JBpALMP5UHum9+7nzR9PeIBCghU9ZtJ1eWm6ieUI8Zb/DI3MYxH32bxkjzYV1LRjNv4qr8d+uX/w== +vue-loader@^15.6.2: + version "15.6.2" + resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.6.2.tgz#892741d96260936ff69e892f72ec361ba4d100d2" + integrity sha512-T6fONodj861M3PqZ1jlbUFjeezbUnPRY2bd+3eZuDvYADgkN3VFU2H5feqySNg9XBt8rcbyBGmFWTZtrOX+v5w== + dependencies: + "@vue/component-compiler-utils" "^2.5.1" + hash-sum "^1.0.2" + loader-utils "^1.1.0" + vue-hot-reload-api "^2.3.0" + vue-style-loader "^4.1.0" + +vue-router@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.2.tgz#dedc67afe6c4e2bc25682c8b1c2a8c0d7c7e56be" + integrity sha512-opKtsxjp9eOcFWdp6xLQPLmRGgfM932Tl56U9chYTnoWqKxQ8M20N7AkdEbM5beUh6wICoFGYugAX9vQjyJLFg== + +vue-rx@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/vue-rx/-/vue-rx-6.1.0.tgz#75c378ef7d58195344b9bf95379d61497c9863ae" + integrity sha512-2nfkmmoZ1C4hBZ6eC1c2vvLzc3DextyNpXIDLQcPfcSHyarQCClyEO5y5Pr/P7MDzT5mrRzfunlLQaH3/lQoXA== vue-style-loader@^3.0.1: version "3.1.2" @@ -13401,7 +13521,7 @@ vue-style-loader@^3.0.1: hash-sum "^1.0.2" loader-utils "^1.0.2" -vue-style-loader@^4.1.0, vue-style-loader@^4.1.2: +vue-style-loader@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.2.tgz#dedf349806f25ceb4e64f3ad7c0a44fba735fcf8" integrity sha512-0ip8ge6Gzz/Bk0iHovU9XAUQaFt/G2B61bnWa2tCcqqdgfHs1lF9xXorFbE55Gmy92okFT+8bfmySuUOu13vxQ== @@ -13478,7 +13598,7 @@ vue-styleguidist@^1.8.9: webpack-dev-server "^2.11.2" webpack-merge "^4.1.3" -vue-template-compiler@2.5.17, vue-template-compiler@^2.4.2, vue-template-compiler@^2.5.16: +vue-template-compiler@^2.4.2, vue-template-compiler@^2.5.16: version "2.5.17" resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.17.tgz#52a4a078c327deb937482a509ae85c06f346c3cb" integrity sha512-63uI4syCwtGR5IJvZM0LN5tVsahrelomHtCxvRkZPJ/Tf3ADm1U1wG6KWycK3qCfqR+ygM5vewUvmJ0REAYksg== @@ -13486,11 +13606,24 @@ vue-template-compiler@2.5.17, vue-template-compiler@^2.4.2, vue-template-compile de-indent "^1.0.2" he "^1.1.0" +vue-template-compiler@^2.5.22: + version "2.5.22" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.22.tgz#c3d3c02c65f1908205c4fbd3b0ef579e51239955" + integrity sha512-1VTw/NPTUeHNiwhkq6NkFzO7gYLjFCueBN0FX8NEiQIemd5EUMQ5hxrF7O0zCPo5tae+U9S/scETPea+hIz8Eg== + dependencies: + de-indent "^1.0.2" + he "^1.1.0" + vue-template-es2015-compiler@^1.5.3, vue-template-es2015-compiler@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.6.0.tgz#dc42697133302ce3017524356a6c61b7b69b4a18" integrity sha512-x3LV3wdmmERhVCYy3quqA57NJW7F3i6faas++pJQWtknWT+n7k30F4TVdHvCLn48peTJFRvCpxs3UuFPqgeELg== +vue-template-es2015-compiler@^1.8.2: + version "1.8.2" + resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.8.2.tgz#dd73e80ba58bb65dd7a8aa2aeef6089cf6116f2a" + integrity sha512-cliV19VHLJqFUYbz/XeWXe5CO6guzwd0yrrqqp0bmjlMP3ZZULY7fu8RTC4+3lmHwo6ESVDHFDsvjB15hcR5IA== + vue-webpack-loaders@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/vue-webpack-loaders/-/vue-webpack-loaders-1.0.8.tgz#1476bfa1feb40401663b5eb72de83ab611f1f047" @@ -13513,11 +13646,21 @@ vue-webpack-loaders@^1.0.8: vue-style-loader "^3.0.1" vue-template-compiler "^2.5.16" -vue@2.5.17, vue@^2.4.2, vue@^2.5.16: +vue@^2.4.2, vue@^2.5.16: version "2.5.17" resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.17.tgz#0f8789ad718be68ca1872629832ed533589c6ada" integrity sha512-mFbcWoDIJi0w0Za4emyLiW72Jae0yjANHbCVquMKijcavBGypqlF7zHRgMa5k4sesdv7hv2rB4JPdZfR+TPfhQ== +vue@^2.5.22: + version "2.5.22" + resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.22.tgz#3bf88041af08b8539c37b268b70ca79245e9cc30" + integrity sha512-pxY3ZHlXNJMFQbkjEgGVMaMMkSV1ONpz+4qB55kZuJzyJOhn6MSy/YZdzhdnumegNzVTL/Dn3Pp4UrVBYt1j/g== + +vuex@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.1.0.tgz#634b81515cf0cfe976bd1ffe9601755e51f843b9" + integrity sha512-mdHeHT/7u4BncpUZMlxNaIdcN/HIt1GsGG5LKByArvYG/v6DvHcOxvDCts+7SRdCoIRGllK8IMZvQtQXLppDYg== + walk-back@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/walk-back/-/walk-back-3.0.0.tgz#2358787a35da91032dad5e92f80b12370d8795c5" diff --git a/lib/galaxy/web/framework/helpers/grids.py b/lib/galaxy/web/framework/helpers/grids.py index 1538ebc3d5d..33184bacc1a 100644 --- a/lib/galaxy/web/framework/helpers/grids.py +++ b/lib/galaxy/web/framework/helpers/grids.py @@ -654,7 +654,7 @@ class CommunityTagsColumn(TextColumn): def get_value(self, trans, grid, item): return trans.fill_template("/tagging_common.mako", tag_type="community", trans=trans, user=trans.get_user(), tagged_item=item, elt_context=self.grid_name, - in_form=True, input_size="20", tag_click_fn="add_tag_to_grid_filter", use_toggle_link=True) + tag_click_fn="add_tag_to_grid_filter", use_toggle_link=True) def filter(self, trans, user, query, column_filter): """ Modify query to filter model_class by tag. Multiple filters are ANDed. """ @@ -690,8 +690,6 @@ class IndividualTagsColumn(CommunityTagsColumn): user=trans.user, tagged_item=item, elt_context=self.grid_name, - in_form=True, - input_size="20", tag_click_fn="add_tag_to_grid_filter", use_toggle_link=True) diff --git a/lib/galaxy/webapps/galaxy/controllers/tag.py b/lib/galaxy/webapps/galaxy/controllers/tag.py index bc3e9dfe6ae..25e74fdf576 100644 --- a/lib/galaxy/webapps/galaxy/controllers/tag.py +++ b/lib/galaxy/webapps/galaxy/controllers/tag.py @@ -30,8 +30,6 @@ class TagsController(BaseUIController, UsesTagsMixin): user=trans.user, tagged_item=item, elt_context=elt_context, - in_form=False, - input_size="22", tag_click_fn="default_tag_click_fn", use_toggle_link=False) diff --git a/lib/galaxy/webapps/reports/framework/grids.py b/lib/galaxy/webapps/reports/framework/grids.py index 1cd9c69a135..8d40c093cad 100644 --- a/lib/galaxy/webapps/reports/framework/grids.py +++ b/lib/galaxy/webapps/reports/framework/grids.py @@ -573,7 +573,7 @@ class CommunityTagsColumn(TextColumn): def get_value(self, trans, grid, item): return trans.fill_template("/tagging_common.mako", tag_type="community", trans=trans, user=trans.get_user(), tagged_item=item, elt_context=self.grid_name, - in_form=True, input_size="20", tag_click_fn="add_tag_to_grid_filter", use_toggle_link=True) + tag_click_fn="add_tag_to_grid_filter", use_toggle_link=True) def filter(self, trans, user, query, column_filter): """ Modify query to filter model_class by tag. Multiple filters are ANDed. """ @@ -609,8 +609,6 @@ class IndividualTagsColumn(CommunityTagsColumn): user=trans.user, tagged_item=item, elt_context=self.grid_name, - in_form=True, - input_size="20", tag_click_fn="add_tag_to_grid_filter", use_toggle_link=True) diff --git a/templates/tagging_common.mako b/templates/tagging_common.mako index 303bc0a3290..5310f40f1c1 100644 --- a/templates/tagging_common.mako +++ b/templates/tagging_common.mako @@ -1,259 +1,124 @@ +<%namespace file="/display_common.mako" import="get_controller_name" /> <%! - from cgi import escape - from random import random - from math import floor - import six - from galaxy.model import Tag, ItemTagAssociation - from galaxy.web.framework.helpers import iff - from galaxy.util import unicodify + +from cgi import escape +import six +## from galaxy.model import Tag, ItemTagAssociation +from galaxy.util import unicodify + +## Build dict of tag name, values. +def tags_to_dict(item_tags): + tag_names_and_values = dict() + for tag in item_tags: + tag_name = escape( tag.user_tname ) + tag_value = "" + if tag.value is not None: + tag_value = escape( tag.user_value ) + + ## Tag names and values may be string or unicode object. + if isinstance( tag_name, six.binary_type ): + tag_names_and_values[unicodify(tag_name, 'utf-8')] = unicodify(tag_value, 'utf-8') + else: + tag_names_and_values[tag_name] = tag_value + return tag_names_and_values + %> ## Render a tagging element if there is a tagged_item. %if tagged_item is not None: %if tag_type == "individual": - ${render_individual_tagging_element( user=user, tagged_item=tagged_item, elt_context=elt_context, in_form=in_form, input_size=input_size, tag_click_fn=tag_click_fn, use_toggle_link=use_toggle_link )} + ${render_individual_tagging_element( + user=user, + tagged_item=tagged_item, + elt_context=elt_context, + tag_click_fn=tag_click_fn, + use_toggle_link=use_toggle_link + )} %elif tag_type == "community": - ${render_community_tagging_element(tagged_item=tagged_item, elt_context=elt_context, tag_click_fn=tag_click_fn)} + ${render_community_tagging_element( + tagged_item=tagged_item, + elt_context=elt_context, + tag_click_fn=tag_click_fn + )} %endif %endif -## Render HTML for a list of tags. -<%def name="render_tagging_element_html(elt_id=None, tags=None, editable=True, use_toggle_link=True, input_size='15', in_form=False, tag_type='individual', render_add_tag_button=True)"> - ## Useful attributes. + +<%def name="render_community_tagging_element( + tagged_item=None, + elt_context=None, + use_toggle_link=False, + tag_click_fn='default_tag_click_fn')"> + <% - num_tags = len( tags ) - %> - - - -## Render tool tagging elements -<%def name="render_tool_tagging_elements()"> - <% - elt_id = int ( floor ( random() * six.MAXSIZE ) ) - tags = trans.app.tag_handler.get_tool_tags() - %> - ${self.render_tagging_element_html(elt_id=elt_id, \ - tags=tags, \ - editable=False, \ - use_toggle_link=False )} - - - -## Render community tagging element. -<%def name="render_community_tagging_element(tagged_item=None, elt_context=None, use_toggle_link=False, tag_click_fn='default_tag_click_fn')"> - ## Build HTML. - <% - elt_id = int ( floor ( random() * six.MAXSIZE ) ) + controller_name = get_controller_name(tagged_item) + click_url = h.url_for( controller='/' + controller_name , action='list_published') community_tags = trans.app.tag_handler.get_community_tags( item=tagged_item, limit=5 ) %> - ${self.render_tagging_element_html(elt_id=elt_id, \ - tags=community_tags, \ - use_toggle_link=use_toggle_link, \ - editable=False, tag_type="community")} + +
- ## Set up tag click function. -## Render individual tagging element. -<%def name="render_individual_tagging_element(user=None, tagged_item=None, elt_context=None, use_toggle_link=True, in_form=False, input_size='15', tag_click_fn='default_tag_click_fn', get_toggle_link_text_fn='default_get_toggle_link_text_fn', editable=True, render_add_tag_button=True)"> - ## Useful attributes. - <% - # Useful ids. - tagged_item_id = str( trans.security.encode_id ( tagged_item.id ) ) - elt_id = int ( floor ( random() * six.MAXSIZE ) ) +<%def name="render_individual_tagging_element( + user=None, + tagged_item=None, + elt_context=None, + use_toggle_link=True, + tag_click_fn='default_tag_click_fn', + get_toggle_link_text_fn='default_get_toggle_link_text_fn', + editable=True)"> - # Get list of user's item tags. TODO: implement owner_tags for all taggable objects and use here. + <% + tagged_item_id = str( trans.security.encode_id ( tagged_item.id ) ) item_tags = [ tag for tag in tagged_item.tags if ( tag.user == user ) ] %> - ## Build HTML. - <% - if len(item_tags) > 3: - # If item has more than 3 tags show a link to see tags instead of displaying them all - use_toggle_link = True - else: - use_toggle_link = False - %> - ${self.render_tagging_element_html(elt_id=elt_id, tags=item_tags, editable=editable, use_toggle_link=use_toggle_link, input_size=input_size, in_form=in_form, render_add_tag_button=render_add_tag_button)} +
- ## Build script that augments tags using progressive javascript. - ## Use style to hide/display the tag area. - - - <%def name="community_tag_js( controller_name )"> +## TODO: Note that this function no longer has anything to do with community +## tags. the ratings code and tagging initialization were previously co-mingled +## in here. Will remove this script when we write the ratings components + ## set up comminity tag and rating handling - used for page start up / set up ## controller_name: the model controller for the item being tagged - generally gotten with get_controller_name( item ) - + diff --git a/client/galaxy/scripts/components/Tags/tagStyles.scss b/client/galaxy/scripts/components/Tags/tagStyles.scss deleted file mode 100644 index a856ec8ecf8..00000000000 --- a/client/galaxy/scripts/components/Tags/tagStyles.scss +++ /dev/null @@ -1,77 +0,0 @@ -// TODO: use general color definition file -@import "theme/blue"; -@import "scss/mixins"; - -// Puts a little graphic in place of the text-input -// when the input is not in focus -@mixin newTagHoverButton() { - .vue-tags-input .ti-tags .ti-new-tag-input-wrapper { - input { - background-color: transparent; - } - input:not(:focus) { - background: url("/static/images/fugue/tag--plus.png"); - background-repeat: no-repeat; - color: transparent; - &::placeholder { - color: transparent; - } - } - } -} - -// hides tag container edges -@mixin hideEditorBorders() { - .vue-tags-input { - // need to add yet another class to beat the scoping - &.tag-area { - background-color: transparent; - } - .ti-input { - border: none; - } - } -} - -// general style butchering -@mixin matchBootstrapStyling() { - .vue-tags-input { - @include fill(); - .ti-input { - padding: 0; - } - .ti-tag { - border-radius: 4px; - font-size: 0.8rem; - font-weight: 400; - } - } -} - -// Version of the tags that only allow clicking -// existing tags instead of the full editing UI -@mixin forDisplayOnly() { - .vue-tags-input { - .ti-actions, - .ti-new-tag-input-wrapper { - display: none; - } - } -} - -.galaxy-tags { - - // adds in a graphic in place of the text input - @include newTagHoverButton(); - - // match bootstrap tag styles/colors - @include matchBootstrapStyling(); - - // removes input borders (not sure if this happens everywhere) - @include hideEditorBorders(); - - // display-only tags - &.disabled { - @include forDisplayOnly(); - } -} From 4af2bdeaef3d493b7c55f46445aeabff720b1b1e Mon Sep 17 00:00:00 2001 From: Dannon Baker Date: Mon, 4 Mar 2019 10:15:39 -0500 Subject: [PATCH 13/15] Client format. --- .../components/Tags/GalaxyTags.test.js | 23 ++--- .../scripts/components/Tags/GalaxyTags.vue | 47 ++++----- .../components/Tags/StandardTags.test.js | 54 +++++------ .../scripts/components/Tags/StandardTags.vue | 30 +++--- .../galaxy/scripts/components/Tags/model.js | 14 +-- .../scripts/components/Tags/model.test.js | 28 ++---- .../scripts/components/Tags/tagService.js | 22 ++--- .../components/Tags/tagService.test.js | 96 ++++++++----------- client/galaxy/scripts/mvc/grid/grid-view.js | 14 ++- .../galaxy/scripts/store/gridSearchStore.js | 2 +- .../galaxy/scripts/utils/mountVueComponent.js | 6 +- client/galaxy/scripts/utils/redirect.js | 2 +- 12 files changed, 142 insertions(+), 196 deletions(-) diff --git a/client/galaxy/scripts/components/Tags/GalaxyTags.test.js b/client/galaxy/scripts/components/Tags/GalaxyTags.test.js index cbb27ce56ac..00dd607daae 100644 --- a/client/galaxy/scripts/components/Tags/GalaxyTags.test.js +++ b/client/galaxy/scripts/components/Tags/GalaxyTags.test.js @@ -6,49 +6,46 @@ let localVue = createLocalVue(); localVue.filter("localize", value => _l(value)); describe("Tags/GalaxyTags.vue", () => { - const testTags = ["abc", "def", "ghi"]; let wrapper, emitted; - beforeEach(function () { - + beforeEach(function() { wrapper = mount(GalaxyTags, { localVue }); wrapper.setProps({ value: testTags }); emitted = wrapper.emitted(); - }) + }); it("should render a div for each tag", () => { let tags = wrapper.findAll(".ti-tag-center"); assert(tags.length == testTags.length, "Wrong number of tags"); - for(let i=0; i < testTags.length; i++) { - assert(tags.at(i).is('div'), "button not a div"); + for (let i = 0; i < testTags.length; i++) { + assert(tags.at(i).is("div"), "button not a div"); assert(tags.at(i).text() == testTags[i], "rendered tag label doesn't match test data"); } - }) + }); it("should emit a click event when the tag is clicked", () => { let tags = wrapper.findAll(".ti-tag-center > div"); tags.at(0).trigger("click"); assert(emitted["tag-click"], "click event not detected"); assert(emitted["tag-click"].length == 1, "wrong event count"); - }) + }); it("should emit a tag model payload when tag is clicked", () => { let tags = wrapper.findAll(".ti-tag-center > div"); tags.at(0).trigger("click"); let firstEvent = emitted["tag-click"][0]; let firstArg = firstEvent[0]; - assert(firstArg.text = testTags[0], "returned tag model doesn't match test data"); - }) + assert((firstArg.text = testTags[0]), "returned tag model doesn't match test data"); + }); it("should change intermal model representation when new tag list assigned", async () => { assert(wrapper.vm.tagModels.length == 3); let newTags = ["floob", "clown", "hoohah", "doodoo"]; wrapper.setProps({ value: newTags }); assert(wrapper.vm.tagModels.length == newTags.length); - }) - -}) + }); +}); diff --git a/client/galaxy/scripts/components/Tags/GalaxyTags.vue b/client/galaxy/scripts/components/Tags/GalaxyTags.vue index 15feb0f1685..7a88f6364c3 100644 --- a/client/galaxy/scripts/components/Tags/GalaxyTags.vue +++ b/client/galaxy/scripts/components/Tags/GalaxyTags.vue @@ -1,12 +1,11 @@ diff --git a/client/galaxy/scripts/components/Tags/StandardTags.test.js b/client/galaxy/scripts/components/Tags/StandardTags.test.js index b0b11edc444..1d0f45d65e0 100644 --- a/client/galaxy/scripts/components/Tags/StandardTags.test.js +++ b/client/galaxy/scripts/components/Tags/StandardTags.test.js @@ -1,5 +1,5 @@ import sinon from "sinon"; -import { mount, createLocalVue} from "@vue/test-utils"; +import { mount, createLocalVue } from "@vue/test-utils"; import StandardTags from "./StandardTags"; import store from "../../store"; import _l from "utils/localization"; @@ -7,9 +7,7 @@ import _l from "utils/localization"; let mockRedirect = sinon.stub(); StandardTags.__Rewire__("redirectToUrl", mockRedirect); - describe("Tags/StandardTags.vue", () => { - let localVue = createLocalVue(); localVue.filter("localize", value => _l(value)); @@ -18,18 +16,16 @@ describe("Tags/StandardTags.vue", () => { function clickFirstTag() { let firstTag = wrapper.find(".ti-tag-center > div"); - firstTag.trigger('click'); + firstTag.trigger("click"); } - + // TODO: put this in a store-specific test file it("the searchTags in the store should be a Set object", () => { let searchTags = store.state.gridSearch.searchTags; // this is a Set() assert(searchTags instanceof Set, "searchTags wrong variable type, should be Set()"); - }) - + }); describe("grid tags (tagClickFn: add_tag_to_grid_filter)", () => { - let propsData = { tags: Array.from(testTags), tagClickFn: "add_tag_to_grid_filter", @@ -37,55 +33,53 @@ describe("Tags/StandardTags.vue", () => { itemClass: "fakeItemClass" }; - beforeEach(function () { - wrapper = mount(StandardTags, { - store, + beforeEach(function() { + wrapper = mount(StandardTags, { + store, propsData, localVue }); - }) + }); afterEach(function() { mockRedirect.reset(); store.dispatch("clearSearchTags"); - }) + }); it("clicking on a search tag should put that tag in the global store", () => { clickFirstTag(); let searchTags = store.state.gridSearch.searchTags; // this is a Set() assert(searchTags.has(testTags[0]), "clicked tag not in store"); assert(searchTags.size == 1, `wrong number of searchTags in store: ${searchTags.size}`); - }) + }); it("clicking the same tag twice should add and remove it from the global store", () => { clickFirstTag(); clickFirstTag(); let searchTags = store.state.gridSearch.searchTags; assert(!searchTags.has(testTags[0]), "clicked tag shouldn't be in store"); - }) + }); it("clicking any odd number of times should put the tag in the store", () => { let oddNumber = 2 * Math.floor(Math.random() * 10) + 1; - for(let i=0; i < oddNumber; i++) { + for (let i = 0; i < oddNumber; i++) { clickFirstTag(); } let searchTags = store.state.gridSearch.searchTags; assert(searchTags.has(testTags[0]), "clicked tag not in store"); - }) + }); it("clicking any even number of times should remove the tag from the store", () => { let evenNumber = 2 * Math.floor(Math.random() * 10); - for(let i=0; i < evenNumber; i++) { + for (let i = 0; i < evenNumber; i++) { clickFirstTag(); } let searchTags = store.state.gridSearch.searchTags; assert(!searchTags.has(testTags[0]), "clicked tag shouldn't be in store"); - }) - - }) + }); + }); describe("community tag (tagClickFn: community_tag_click)", () => { - let propsData = { tags: ["abc", "def"], tagClickFn: "community_tag_click", @@ -94,26 +88,24 @@ describe("Tags/StandardTags.vue", () => { itemClass: "fakeItemClass" }; - beforeEach(function () { - wrapper = mount(StandardTags, { + beforeEach(function() { + wrapper = mount(StandardTags, { store, propsData, localVue }); - }) + }); afterEach(function() { mockRedirect.reset(); store.dispatch("clearSearchTags"); - }) + }); // tagClickFn=community_tag_click, clickUrl=something it("should try to redirect when you click a 'community tag'", () => { clickFirstTag(); let expectedUrl = `${propsData.clickUrl}?f-tags=${testTags[0]}`; assert(mockRedirect.calledWith(expectedUrl), "requested wrong url"); - }) - - }) - -}) + }); + }); +}); diff --git a/client/galaxy/scripts/components/Tags/StandardTags.vue b/client/galaxy/scripts/components/Tags/StandardTags.vue index 9d2e88a3420..08a82675e05 100644 --- a/client/galaxy/scripts/components/Tags/StandardTags.vue +++ b/client/galaxy/scripts/components/Tags/StandardTags.vue @@ -9,7 +9,8 @@ parameters here. -->