mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #7270 from Nerdinacan/tagging_component_3
Tagging component
This commit is contained in:
@@ -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/*"]
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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:
|
||||
|
||||
<galaxy-tags
|
||||
v-model="tags"
|
||||
:autocomplete-items="autocompleteItems"
|
||||
@tag-click="tagClick"
|
||||
@tag-input-changed="tagTextChanged"
|
||||
@before-adding-tag="beforeAddingTag"
|
||||
@before-deleting-tag="beforeDeletingTag"
|
||||
/>
|
||||
|
||||
...
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div :class="tagContainerClasses">
|
||||
<a href="#" class="toggle-link" v-if="linkVisible" @click.prevent="toggleTagDisplay">
|
||||
{{ linkText | localize }}
|
||||
</a>
|
||||
<vue-tags-input
|
||||
class="tag-area"
|
||||
v-if="tagsVisible"
|
||||
v-model="tagText"
|
||||
:tags="tagModels"
|
||||
:autocomplete-items="autocompleteTags"
|
||||
:disabled="disabled"
|
||||
:placeholder="'Add Tags' | localize"
|
||||
:add-on-key="[13, ' ']"
|
||||
@before-adding-tag="beforeAddingTag"
|
||||
@before-deleting-tag="beforeDeletingTag"
|
||||
@tags-changed="tagsChanged"
|
||||
>
|
||||
<div class="tag-name" slot="tag-center" slot-scope="tagProps" @click="$emit('tag-click', tagProps.tag)">
|
||||
{{ tagProps.tag.text }}
|
||||
</div>
|
||||
</vue-tags-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VueTagsInput from "@johmun/vue-tags-input";
|
||||
import { createTag } from "./model";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
VueTagsInput
|
||||
},
|
||||
props: {
|
||||
value: { type: Array, required: false, default: () => [] },
|
||||
autocompleteItems: { type: Array, required: false, default: () => [] },
|
||||
maxVisibleTags: { type: Number, required: false, default: 5 },
|
||||
useToggleLink: { type: Boolean, required: false, default: true },
|
||||
disabled: { type: Boolean, required: false, default: false }
|
||||
},
|
||||
data() {
|
||||
// initialize toggle value
|
||||
let isClosed = this.useToggleLink && this.value.length > this.maxVisibleTags;
|
||||
|
||||
return {
|
||||
// text of the new tag, when editable
|
||||
tagText: "",
|
||||
// if list is too long and we're using the toggle, then hide the tags
|
||||
tagToggle: !isClosed
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tagContainerClasses() {
|
||||
return {
|
||||
"galaxy-tags": true,
|
||||
disabled: this.disabled
|
||||
};
|
||||
},
|
||||
tagModels() {
|
||||
return this.value.map(createTag);
|
||||
},
|
||||
autocompleteTags() {
|
||||
return this.autocompleteItems.map(createTag);
|
||||
},
|
||||
linkText() {
|
||||
return `${this.tagModels.length} Tags`;
|
||||
},
|
||||
linkVisible() {
|
||||
return this.useToggleLink && this.tagModels.length > this.maxVisibleTags;
|
||||
},
|
||||
tagsVisible() {
|
||||
return this.useToggleLink ? this.tagToggle : true;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
tagText(newValue) {
|
||||
this.$emit("tag-input-changed", newValue);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
tagsChanged(newTags) {
|
||||
this.$emit("input", this.pluckLabels(newTags));
|
||||
},
|
||||
pluckLabels(newTags) {
|
||||
return newTags.map(t => createTag(t).toString());
|
||||
},
|
||||
toggleTagDisplay() {
|
||||
this.tagToggle = !this.tagToggle;
|
||||
this.$emit("show", this.tagToggle);
|
||||
},
|
||||
beforeAddingTag($event) {
|
||||
if (!this.emitHookEvent("before-adding-tag", $event)) {
|
||||
let { tag, addTag } = $event;
|
||||
addTag(tag);
|
||||
}
|
||||
},
|
||||
beforeDeletingTag($event) {
|
||||
if (!this.emitHookEvent("before-deleting-tag", $event)) {
|
||||
let { tag, deleteTag } = $event;
|
||||
deleteTag(tag);
|
||||
}
|
||||
},
|
||||
emitHookEvent(eventName, $event) {
|
||||
if (this.hasHandler(eventName)) {
|
||||
this.$emit(eventName, $event);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
hasHandler(eventName) {
|
||||
return Object.keys(this.$listeners).includes(eventName);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@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();
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
<!--
|
||||
Wraps up existing functionality for community/indvidual tags. The component
|
||||
wraps the more general "GalaxyTags".
|
||||
|
||||
I expect that "StandardTags" will go away one day as we move more of the site
|
||||
into nested components/views which will implement "GalaxyTags" with appropriate
|
||||
context-specific parameters. Until we can do that, we have to wrangle the python
|
||||
parameters here.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<galaxy-tags
|
||||
v-model="tags"
|
||||
:disabled="disabled"
|
||||
:autocomplete-items="autocompleteItems"
|
||||
@tag-click="clickHandler"
|
||||
@tag-input-changed="updateTagSearch"
|
||||
@before-adding-tag="beforeAddingTag"
|
||||
@before-deleting-tag="beforeDeletingTag"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import VueRx from "vue-rx";
|
||||
import { map } from "rxjs/operators";
|
||||
import GalaxyTags from "./GalaxyTags";
|
||||
import { redirectToUrl } from "utils/redirect";
|
||||
import { buildTagService } from "./tagService";
|
||||
import { diffTags } from "./model";
|
||||
|
||||
Vue.use(VueRx);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GalaxyTags
|
||||
},
|
||||
props: {
|
||||
tags: { type: Array, required: false, default: () => [] },
|
||||
|
||||
// currently we're passing in the click handler name as a property, and
|
||||
// that property is still defined by python, so this will have to stay
|
||||
// for a little bit.
|
||||
tagClickFn: { type: String, required: false, default: "" },
|
||||
|
||||
// used with community_tag_click option
|
||||
clickUrl: { type: String, required: false, default: "" },
|
||||
|
||||
// tag service params (TODO: should inject the service in as a property
|
||||
// instead of constructing it in here from the props)
|
||||
id: { type: String, required: true },
|
||||
itemClass: { type: String, required: true },
|
||||
context: { type: String, required: false, default: "" },
|
||||
debounceInterval: { type: Number, required: false, default: 150 },
|
||||
|
||||
// allows user to add tags
|
||||
disabled: { type: Boolean, required: false, default: false }
|
||||
},
|
||||
computed: {
|
||||
tagService() {
|
||||
return buildTagService(this.$props);
|
||||
}
|
||||
},
|
||||
subscriptions() {
|
||||
return {
|
||||
// return search result tags without the ones we've already selected
|
||||
autocompleteItems: this.tagService.autocompleteOptions.pipe(
|
||||
map(resultTags => diffTags(resultTags, this.tags))
|
||||
)
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
clickHandler(tag) {
|
||||
if (undefined !== this[this.tagClickFn]) {
|
||||
this[this.tagClickFn](tag);
|
||||
}
|
||||
},
|
||||
|
||||
add_tag_to_grid_filter(tag) {
|
||||
this.$store.dispatch("toggleSearchTag", tag);
|
||||
},
|
||||
|
||||
community_tag_click(tag) {
|
||||
// I made this match the existing behavior, but I am not clear on
|
||||
// the reason why this link redirects to a raw json page
|
||||
let suffix = tag.value ? `:${tag.value}` : "";
|
||||
let href = `${this.clickUrl}?f-tags=${tag.text}${suffix}`;
|
||||
redirectToUrl(href);
|
||||
},
|
||||
|
||||
// Hooks to save/delete tag to server before updating UI
|
||||
|
||||
beforeAddingTag({ tag, addTag }) {
|
||||
this.tagService
|
||||
.save(tag)
|
||||
.then(() => addTag(tag))
|
||||
.catch(err => console.warn("unable to save tag", err));
|
||||
},
|
||||
|
||||
beforeDeletingTag({ tag, deleteTag }) {
|
||||
this.tagService
|
||||
.delete(tag)
|
||||
.then(result => deleteTag(tag))
|
||||
.catch(err => console.warn("Unable to delete tag", err));
|
||||
},
|
||||
|
||||
// Set search value on tag service input proprety and eventually search
|
||||
// results will appear on the tagService.autocompleteOptions observable
|
||||
// object which is subscribed to above
|
||||
|
||||
updateTagSearch(searchTxt) {
|
||||
this.tagService.autocompleteSearchText = searchTxt;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as GalaxyTags } from "./GalaxyTags";
|
||||
export { default as StandardTags } from "./StandardTags";
|
||||
export { mountTaggingComponent } from "./inits";
|
||||
@@ -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);
|
||||
@@ -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)));
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
#Header|Your Tags
|
||||
abc|abc
|
||||
def|def
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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 }
|
||||
});
|
||||
@@ -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 = $("<img/>")
|
||||
.attr("src", settings.delete_tag_img)
|
||||
.addClass("delete-tag-img");
|
||||
init_delete_tag_image(delete_img);
|
||||
|
||||
// Build tag button.
|
||||
var tag_name_elt = $("<span>")
|
||||
.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 = $("<span></span>").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;
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
@@ -1,4 +1,4 @@
|
||||
// console.log api but does nothing
|
||||
// Applies noop to all methods for mock creation
|
||||
|
||||
const doNothing = () => null;
|
||||
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
// This file exists purely to make unit testing easier
|
||||
|
||||
export function redirectToUrl(url) {
|
||||
window.location = url;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Utility mixin expands to container edges
|
||||
@mixin fill() {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
+13
-7
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
+151
-8
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+74
-219
@@ -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 )
|
||||
%>
|
||||
<div class="tag-element"
|
||||
%if elt_id:
|
||||
id="${elt_id}"
|
||||
%endif
|
||||
## Do not display element if there are no tags and it is not editable.
|
||||
%if num_tags == 0 and not editable:
|
||||
style="display: none"
|
||||
%endif
|
||||
>
|
||||
%if use_toggle_link:
|
||||
<a class="toggle-link" href="#">${num_tags} Tag${iff( num_tags == 1, "", "s")}</a>
|
||||
%endif
|
||||
<div class="tag-area
|
||||
%if tag_type == 'individual':
|
||||
individual-tag-area
|
||||
%endif
|
||||
">
|
||||
|
||||
## Build buttons for current tags.
|
||||
%for tag in tags:
|
||||
<%
|
||||
## Handle both Tag and ItemTagAssociation objects.
|
||||
if isinstance( tag, Tag ):
|
||||
tag_name = tag.name
|
||||
tag_value = None
|
||||
elif isinstance( tag, ItemTagAssociation ):
|
||||
tag_name = tag.user_tname
|
||||
tag_value = tag.user_value
|
||||
|
||||
## Convert tag name, value to unicode.
|
||||
if isinstance( tag_name, six.binary_type ):
|
||||
tag_name = unicodify( escape( tag_name ) )
|
||||
if tag_value:
|
||||
tag_value = unicodify( escape( tag_value ) )
|
||||
if tag_value:
|
||||
tag_str = tag_name + ":" + tag_value
|
||||
else:
|
||||
tag_str = tag_name
|
||||
%>
|
||||
<span class="tag-button">
|
||||
<span class="tag-name">${tag_str | h}</span>
|
||||
%if editable:
|
||||
<img class="delete-tag-img" src="${h.url_for('/static/images/delete_tag_icon_gray.png')}"/>
|
||||
%endif
|
||||
</span>
|
||||
%endfor
|
||||
|
||||
## Add tag input field. If element is in form, tag input is a textarea; otherwise element is a input type=text.
|
||||
%if editable:
|
||||
%if in_form:
|
||||
<textarea class="tag-input" rows='1' cols='${input_size}'></textarea>
|
||||
%else:
|
||||
<input class="tag-input" type='text' size='${input_size}'/>
|
||||
%endif
|
||||
## Add "add tag" button.
|
||||
%if render_add_tag_button:
|
||||
<img src='${h.url_for('/static/images/fugue/tag--plus.png')}' class="add-tag-button" title="Add tags"/>
|
||||
%endif
|
||||
%endif
|
||||
</div>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
## 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 )}
|
||||
<script type="text/javascript">
|
||||
window.bundleEntries.init_tag_click_function($('#${elt_id}'), tool_tag_click);
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
## 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")}
|
||||
|
||||
<div id="tags-community-${controller_name}-${tagged_item.id}"></div>
|
||||
|
||||
## Set up tag click function.
|
||||
<script type="text/javascript">
|
||||
window.bundleEntries.init_tag_click_function($('#${elt_id}'), ${tag_click_fn});
|
||||
config.addInitialization(function(galaxy, config) {
|
||||
var container = document.getElementById("tags-community-${controller_name}-${tagged_item.id}");
|
||||
|
||||
var options = ${h.dumps(tagged_item.to_dict())};
|
||||
options.tags = ${h.dumps(community_tag_names)};
|
||||
options.id = String(options.id);
|
||||
options.itemClass = options.model_class;
|
||||
options.context = "${elt_context}";
|
||||
options.tagClickFn = "${tag_click_fn}";
|
||||
options.clickUrl = "${click_url}";
|
||||
options.disabled = true;
|
||||
|
||||
bundleEntries.mountTaggingComponent(options, container);
|
||||
});
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
|
||||
## 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)}
|
||||
<div id="tags-${tagged_item_id}"></div>
|
||||
|
||||
## Build script that augments tags using progressive javascript.
|
||||
<script type="text/javascript">
|
||||
//
|
||||
// Set up autocomplete tagger.
|
||||
//
|
||||
config.addInitialization(function(galaxy, config) {
|
||||
var container = document.getElementById("tags-${tagged_item_id}");
|
||||
|
||||
//
|
||||
// Default function get text to display on the toggle link.
|
||||
//
|
||||
var default_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");
|
||||
/*
|
||||
// Show first N tags; hide the rest.
|
||||
var max_to_show = 1;
|
||||
var options = ${h.dumps(tagged_item.to_dict())};
|
||||
options.tags = ${h.dumps(item_tag_names)};
|
||||
options.id = "${tagged_item_id}";
|
||||
options.itemClass = "${tagged_item.__class__.__name__}";
|
||||
options.context = "${elt_context}";
|
||||
options.tagClickFn = "${tag_click_fn}";
|
||||
options.disabled = !${h.to_js_bool(editable)};
|
||||
|
||||
// Build tag string.
|
||||
var tag_strs = new Array();
|
||||
var count = 0;
|
||||
for (tag_name in tags)
|
||||
{
|
||||
tag_value = tags[tag_name];
|
||||
tag_strs[tag_strs.length] = build_tag_str(tag_name, tag_value);
|
||||
if (++count == max_to_show)
|
||||
break;
|
||||
}
|
||||
tag_str = tag_strs.join(", ");
|
||||
|
||||
// Finalize text.
|
||||
var num_tags_hiding = num_tags - max_to_show;
|
||||
text = "Tags: " + tag_str +
|
||||
(num_tags_hiding != 0 ? " and " + num_tags_hiding + " more" : "");
|
||||
*/
|
||||
}
|
||||
else
|
||||
{
|
||||
// No tags.
|
||||
text = "Add tags";
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
// Default function to handle a tag click.
|
||||
var default_tag_click_fn = function(tag_name, tag_value) { };
|
||||
|
||||
<%
|
||||
## Build dict of tag name, values.
|
||||
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
|
||||
%>
|
||||
var options =
|
||||
{
|
||||
tags : ${h.dumps(tag_names_and_values)},
|
||||
editable : ${iff( editable, 'true', 'false' )},
|
||||
get_toggle_link_text_fn: ${get_toggle_link_text_fn},
|
||||
tag_click_fn: ${tag_click_fn},
|
||||
## Use forward slash in controller to suppress route memory.
|
||||
ajax_autocomplete_tag_url: "${h.url_for( controller='/tag', action='tag_autocomplete_data', item_id=tagged_item_id, item_class=tagged_item.__class__.__name__ )}",
|
||||
ajax_add_tag_url: "${h.url_for( controller='/tag', action='add_tag_async', item_id=tagged_item_id, item_class=tagged_item.__class__.__name__, context=elt_context )}",
|
||||
ajax_delete_tag_url: "${h.url_for( controller='/tag', action='remove_tag_async', item_id=tagged_item_id, item_class=tagged_item.__class__.__name__, context=elt_context )}",
|
||||
delete_tag_img: "${h.url_for('/static/images/delete_tag_icon_gray.png')}",
|
||||
delete_tag_img_rollover: "${h.url_for('/static/images/delete_tag_icon_white.png')}",
|
||||
use_toggle_link: ${iff( use_toggle_link, 'true', 'false' )}
|
||||
};
|
||||
|
||||
$('#${elt_id}').autocomplete_tagging(options);
|
||||
bundleEntries.mountTaggingComponent(options, container);
|
||||
});
|
||||
</script>
|
||||
|
||||
## Use style to hide/display the tag area.
|
||||
<style>
|
||||
.tag-area {
|
||||
display: ${iff( use_toggle_link, "none", "block" )};
|
||||
}
|
||||
</style>
|
||||
|
||||
<noscript>
|
||||
<style>
|
||||
.tag-area {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</noscript>
|
||||
</%def>
|
||||
|
||||
|
||||
<%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 )
|
||||
<script type="text/javascript">
|
||||
// Handle click on community tag.
|
||||
function community_tag_click(tag_name, tag_value) {
|
||||
var href = '${h.url_for ( controller='/' + controller_name , action='list_published')}';
|
||||
href = href + "?f-tags=" + tag_name;
|
||||
if (tag_value != undefined && tag_value != "") {
|
||||
href = href + ":" + tag_value;
|
||||
}
|
||||
self.location = href;
|
||||
}
|
||||
|
||||
// Map item rating to number of stars to show.
|
||||
function map_rating_to_num_stars(rating) {
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
Tags:
|
||||
</label>
|
||||
<div style="float: left; width: 225px; margin-right: 10px; border-style: inset; border-width: 1px; margin-left: 2px">
|
||||
${render_individual_tagging_element(user=trans.get_user(), tagged_item=stored, elt_context="edit_attributes.mako", use_toggle_link=False, input_size="20")}
|
||||
${render_individual_tagging_element(user=trans.get_user(), tagged_item=stored, elt_context="edit_attributes.mako", use_toggle_link=False)}
|
||||
</div>
|
||||
<div class="toolParamHelp">Apply tags to make it easy to search for and find items with the same tag.</div>
|
||||
</div>
|
||||
|
||||
@@ -95,7 +95,7 @@ class HistoryGridTestCase(SharedStateSeleniumTestCase):
|
||||
if tags is None:
|
||||
raise AssertionError('Failed to find history with name [%s]' % self.history1_name)
|
||||
|
||||
tag_button_selector = '.tag-area > .tag-button > .tag-name'
|
||||
tag_button_selector = 'div.tag-name'
|
||||
tag_buttons = tags.find_elements_by_css_selector(tag_button_selector)
|
||||
tag_button_text = None
|
||||
target_tag_button_text = self.history1_tags[0]
|
||||
|
||||
@@ -211,20 +211,16 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase):
|
||||
self._login()
|
||||
self.navigate_to_saved_histories_page()
|
||||
|
||||
# Click the add tag button
|
||||
tags_cell = self.get_history_tags_cell(self.history2_name)
|
||||
add_tag_button = tags_cell.find_element_by_css_selector('.add-tag-button')
|
||||
add_tag_button.click()
|
||||
|
||||
# Insert a tag
|
||||
tags_cell = self.get_history_tags_cell(self.history2_name)
|
||||
tag_area = tags_cell.find_element_by_tag_name('textarea')
|
||||
tag_area = tags_cell.find_element_by_css_selector('.ti-new-tag-input-wrapper input')
|
||||
tag_area.click()
|
||||
tag_area.send_keys(self.history2_tags[0])
|
||||
self.send_enter(tag_area)
|
||||
|
||||
# Search by tag
|
||||
tags_cell = self.get_history_tags_cell(self.history2_name)
|
||||
tag = tags_cell.find_element_by_css_selector('span.tag-name')
|
||||
tag = tags_cell.find_element_by_css_selector('.ti-tag-center')
|
||||
tag.click()
|
||||
|
||||
self.assert_grid_histories_are([self.history2_name], False)
|
||||
|
||||
Reference in New Issue
Block a user