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..f25b8f383f7 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"; +export { default as store } from "storemodern"; 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..00dd607daae --- /dev/null +++ b/client/galaxy/scripts/components/Tags/GalaxyTags.test.js @@ -0,0 +1,51 @@ +import { mount, createLocalVue } from "@vue/test-utils"; +import GalaxyTags from "./GalaxyTags"; +import _l from "utils/localization"; + +let localVue = createLocalVue(); +localVue.filter("localize", value => _l(value)); + +describe("Tags/GalaxyTags.vue", () => { + const testTags = ["abc", "def", "ghi"]; + + let wrapper, emitted; + + 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"); + 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.usage.txt b/client/galaxy/scripts/components/Tags/GalaxyTags.usage.txt new file mode 100644 index 00000000000..f603be6b1c5 --- /dev/null +++ b/client/galaxy/scripts/components/Tags/GalaxyTags.usage.txt @@ -0,0 +1,70 @@ +GalaxyTags.vue + +Configurable tag component. Needed to do some minor event/property remapping +to make the 3rd party component suit the interface I wanted to expose. This +component is used like a glorified form input with v-model. Set v-model +to the array of selected tags. + +Props: + "value": + Standard vue property that is assigned when v-model="tags" is set. + No need to set it directly, just use v-model="tags" + autoCompleteItems: + An array of autocomplete items for the current tag text. You can capture + changes to the tag being entered in the tag-input-changed event and reset + this property as required + +Events: + "input" (standard v-model update event, Not used directly): + Emits when list of tags change + before-adding-tag({ tag, addTag }) + Hook allowing user the chance to do an operation before + commiting a tag to the list + before-deleting-tag({ tag, deleteTag }) + Hook allowing consumer to do somehting before removing + a tag from the displayed list. + tag-click: + When the central name display of the tag is clicked, can + assign a handler from the consuming environment + tag-input-changed: + As user is typing a new tag, the text gets emitted here. + +Usage: + + + + ... + + methods: { + + // tag is only added when addTag(tag) is called + beforeAddingTag({ tag, addTag }) { + saveTagToServer().then(() => addTag(tag)); + }, + + // tag is deleted when deleteTag(tag) is called + beforeDeletingTag({ tag, deleteTag }) { + deleteTagFromServer().then(() => deleteTag(tag)); + }, + + // do a database lookup to generate viable options and + // set on the property that's passed to the component to + // do a little autocomplete dropdown + tagTextChanged(txt) { + generateAutocompleteOptions(txt).then((newOptions) => { + this.autocompleteItems = newOptions; + }) + }, + + // any appropriate click handler (update search, etc.) + tagClick(tag) { + // do something with the tag data + } + } diff --git a/client/galaxy/scripts/components/Tags/GalaxyTags.vue b/client/galaxy/scripts/components/Tags/GalaxyTags.vue new file mode 100644 index 00000000000..7a88f6364c3 --- /dev/null +++ b/client/galaxy/scripts/components/Tags/GalaxyTags.vue @@ -0,0 +1,193 @@ + + + + + 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..1d0f45d65e0 --- /dev/null +++ b/client/galaxy/scripts/components/Tags/StandardTags.test.js @@ -0,0 +1,111 @@ +import sinon from "sinon"; +import { mount, createLocalVue } from "@vue/test-utils"; +import StandardTags from "./StandardTags"; +import store from "../../store"; +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)); + + 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, + 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++) { + 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, + 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 new file mode 100644 index 00000000000..08a82675e05 --- /dev/null +++ b/client/galaxy/scripts/components/Tags/StandardTags.vue @@ -0,0 +1,116 @@ + + + + + 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..94ceeb76b42 --- /dev/null +++ b/client/galaxy/scripts/components/Tags/model.js @@ -0,0 +1,42 @@ +/** + * 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..6dccc65b0ba --- /dev/null +++ b/client/galaxy/scripts/components/Tags/model.test.js @@ -0,0 +1,52 @@ +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..e868fe19504 --- /dev/null +++ b/client/galaxy/scripts/components/Tags/tagService.js @@ -0,0 +1,107 @@ +/** + * 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..95d66d74966 --- /dev/null +++ b/client/galaxy/scripts/components/Tags/tagService.test.js @@ -0,0 +1,138 @@ +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/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..47e0ee120ec 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) { @@ -99,6 +114,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)); this.$el.find("#grid-table-body").html(Templates.body(options)); @@ -310,6 +328,14 @@ 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,10 +350,6 @@ 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. @@ -335,6 +357,11 @@ export default Backbone.View.extend({ // 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(); this.execute(); @@ -665,5 +692,12 @@ 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..e67ddaaf92d --- /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..233efabf329 --- /dev/null +++ b/client/galaxy/scripts/store/index.js @@ -0,0 +1,13 @@ +/** + * Central Vuex store + */ + +import Vue from "vue"; +import Vuex from "vuex"; +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..03889b99a1c --- /dev/null +++ b/client/galaxy/scripts/utils/mountVueComponent.js @@ -0,0 +1,15 @@ +// Generic Vue component mount for use in transitional +// mount functions + +import Vue from "vue"; +import store from "../store"; +import _l from "utils/localization"; + +// make localization filter available to all components +Vue.filter("localize", value => _l(value)); + +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..e4fb137d8b2 --- /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; +} diff --git a/client/galaxy/style/scss/base.scss b/client/galaxy/style/scss/base.scss index cac5824b3ae..95c0cee748c 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..140aee29b7f 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..70b644835df 100644 --- a/client/webpack.config.js +++ b/client/webpack.config.js @@ -29,7 +29,7 @@ let buildconfig = { alias: { jquery$: `${libsBase}/jquery.custom.js`, jqueryVendor$: `${libsBase}/jquery/jquery.js`, - store$: "store/dist/store.modern.js" + storemodern$: "store/dist/store.modern.js" } }, optimization: { @@ -64,7 +64,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 +159,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 683430d19d4..08f7603b2c4 100644 --- a/lib/galaxy/web/framework/helpers/grids.py +++ b/lib/galaxy/web/framework/helpers/grids.py @@ -644,7 +644,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. """ @@ -680,8 +680,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 f012b951829..677afa43847 100644 --- a/lib/galaxy/webapps/reports/framework/grids.py +++ b/lib/galaxy/webapps/reports/framework/grids.py @@ -563,7 +563,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. """ @@ -599,8 +599,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..078322fa64c 100644 --- a/templates/tagging_common.mako +++ b/templates/tagging_common.mako @@ -1,259 +1,114 @@ +<%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 %> ## 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 ) + + ## Having trouble converting list of tags into a plain array, this just + ## just plucks out the name + community_tag_names = [] + for tag in community_tags: + community_tag_names.append(escape(tag.name)) %> - ${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. +<%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)"> + <% - # Useful ids. tagged_item_id = str( trans.security.encode_id ( tagged_item.id ) ) - elt_id = int ( floor ( random() * six.MAXSIZE ) ) - - # Get list of user's item tags. TODO: implement owner_tags for all taggable objects and use here. item_tags = [ tag for tag in tagged_item.tags if ( tag.user == user ) ] + + item_tag_names = [] + for ta in item_tags: + item_tag_names.append(escape(ta.tag.name)) %> - ## 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 )