mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Remove legacy Tags component
This commit is contained in:
@@ -42,7 +42,5 @@ export function chart(options) {
|
||||
return new Client(options);
|
||||
}
|
||||
|
||||
export { mountMakoTags } from "components/Tags";
|
||||
|
||||
// Used in common.mako
|
||||
export { default as store } from "storemodern";
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { tagStore } from "store/tagStore";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
import Vuex from "vuex";
|
||||
|
||||
import Tags from "./Tags";
|
||||
import { TagService } from "./tagService";
|
||||
|
||||
jest.mock("./tagService");
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
const testStore = new Vuex.Store({
|
||||
modules: {
|
||||
tagStore,
|
||||
},
|
||||
});
|
||||
|
||||
describe("Tags/Tags.vue", () => {
|
||||
const id = "testId";
|
||||
const itemClass = "TestObject";
|
||||
const context = "testing";
|
||||
let tagService;
|
||||
let wrapper;
|
||||
let emitted;
|
||||
const startingTags = ["a", "b", "c"];
|
||||
const storeKey = "testingTagSet";
|
||||
|
||||
const clickFirstTag = () => {
|
||||
if (wrapper) {
|
||||
const firstTag = wrapper.find(".ti-tag-center > div");
|
||||
firstTag.trigger("click");
|
||||
} else {
|
||||
console.log("missing tag");
|
||||
}
|
||||
};
|
||||
|
||||
// Run before each as async so the lifecycle methods can run
|
||||
beforeEach(async () => {
|
||||
// Create the (mocked, above) TagService
|
||||
tagService = new TagService({ id, itemClass, context });
|
||||
|
||||
// Mount the tags with sample props
|
||||
|
||||
wrapper = mount(Tags, {
|
||||
store: testStore,
|
||||
localVue,
|
||||
propsData: {
|
||||
tags: startingTags,
|
||||
storeKey,
|
||||
tagService,
|
||||
},
|
||||
});
|
||||
|
||||
emitted = wrapper.emitted();
|
||||
|
||||
// Waits for lifecycle handlers to execute
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tagService = null;
|
||||
});
|
||||
|
||||
it("should display the tags I give it", async () => {
|
||||
const tags = wrapper.findAll(".ti-tag-center .tag-name");
|
||||
expect(tags.length).toBe(3);
|
||||
expect(tags.at(0).text()).toBe(startingTags[0]);
|
||||
});
|
||||
|
||||
it("should put the initialized tags into the store at initialization", async () => {
|
||||
const storedTags = testStore.getters.getTagsById(storeKey);
|
||||
expect(storedTags.length).toBe(3);
|
||||
startingTags.forEach((tag, i) => {
|
||||
expect(tag).toBe(storedTags[i]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should respond to click events on the tags", async () => {
|
||||
clickFirstTag();
|
||||
expect(emitted["tag-click"]).toBeTruthy();
|
||||
expect(emitted["tag-click"].length == 1).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should reflect changes in the store", async () => {
|
||||
const newTags = ["asdfadsadf", "gfhjfghjf"];
|
||||
testStore.dispatch("updateTags", { key: storeKey, tags: newTags });
|
||||
|
||||
await wrapper.setProps({ storeKey });
|
||||
|
||||
const observed = wrapper.vm.observedTags;
|
||||
expect(observed.length).toBe(newTags.length);
|
||||
observed.forEach((tagLabel, i) => {
|
||||
expect(newTags[i]).toBe(tagLabel);
|
||||
});
|
||||
});
|
||||
|
||||
describe("autocomplete list", () => {
|
||||
let subscription;
|
||||
|
||||
afterEach(() => {
|
||||
if (subscription) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
it("should generate autocomplete items when you set the search text", (done) => {
|
||||
const sampleTxt = "floobar";
|
||||
subscription = tagService.autocompleteOptions.subscribe((results) => {
|
||||
expect(results.length).toBe(1);
|
||||
results.forEach((r) => {
|
||||
expect(r.text).toBe(sampleTxt);
|
||||
});
|
||||
done();
|
||||
});
|
||||
tagService.autocompleteSearchText = sampleTxt;
|
||||
});
|
||||
|
||||
it("should debounce autocomplete requests", (done) => {
|
||||
const sampleTxt = "floobar";
|
||||
subscription = tagService.autocompleteOptions.subscribe((results) => {
|
||||
expect(results.length).toBe(1);
|
||||
results.forEach((r) => {
|
||||
expect(r.text).toBe(sampleTxt);
|
||||
});
|
||||
done();
|
||||
});
|
||||
tagService.autocompleteSearchText = "foo";
|
||||
tagService.autocompleteSearchText = sampleTxt;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,99 +0,0 @@
|
||||
<template>
|
||||
<StatelessTags
|
||||
v-model="observedTags"
|
||||
:disabled="disabled"
|
||||
:autocomplete-items="autocompleteItems"
|
||||
@tag-click="onClick"
|
||||
@tag-input-changed="updateTagSearch"
|
||||
@before-adding-tag="beforeAddingTag"
|
||||
@before-deleting-tag="beforeDeletingTag" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { map } from "rxjs/operators";
|
||||
import Vue from "vue";
|
||||
import VueRx from "vue-rx";
|
||||
import { mapActions } from "vuex";
|
||||
|
||||
import { diffTags } from "./model";
|
||||
import StatelessTags from "./StatelessTags";
|
||||
import { TagService } from "./tagService";
|
||||
|
||||
Vue.use(VueRx);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StatelessTags,
|
||||
},
|
||||
|
||||
props: {
|
||||
// initialization value
|
||||
tags: { type: Array, required: false, default: () => [] },
|
||||
|
||||
// data requests go through this object
|
||||
tagService: { type: TagService, required: true },
|
||||
|
||||
// store key, usually a model ID or something like that
|
||||
storeKey: { type: String, required: true },
|
||||
|
||||
// allows user to edit tag list
|
||||
disabled: { type: Boolean, required: false, default: false },
|
||||
},
|
||||
|
||||
computed: {
|
||||
observedTags: {
|
||||
get() {
|
||||
return this.$store.getters.getTagsById(this.storeKey);
|
||||
},
|
||||
set(tags) {
|
||||
this.updateTags({ key: this.storeKey, tags });
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
subscriptions() {
|
||||
return {
|
||||
autocompleteItems: this.tagService.autocompleteOptions.pipe(
|
||||
// without the ones we've already selected
|
||||
map((resultTags) => diffTags(resultTags, this.tags))
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.initializeTags({ key: this.storeKey, tags: this.tags });
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick(tag) {
|
||||
this.$emit("tag-click", tag);
|
||||
},
|
||||
|
||||
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(() => 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) {
|
||||
// TODO: Rework tagService prop mutation
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
this.tagService.autocompleteSearchText = searchTxt;
|
||||
},
|
||||
|
||||
...mapActions(["updateTags", "initializeTags"]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,60 +0,0 @@
|
||||
/**
|
||||
* 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 the associated python endpoint to a legit json REST service
|
||||
*/
|
||||
|
||||
import { Subject } from "rxjs";
|
||||
import { debounceTime, distinctUntilChanged, filter, map, switchMap } from "rxjs/operators";
|
||||
|
||||
import { createTag } from "../model";
|
||||
|
||||
export class TagService {
|
||||
constructor({ id, itemClass, context, debounceInterval = 150 }) {
|
||||
this.id = id;
|
||||
this.itemClass = itemClass;
|
||||
this.context = context;
|
||||
this.debounceInterval = debounceInterval;
|
||||
|
||||
// Buffer for autocomplete text changes
|
||||
this._searchText = new Subject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Autocomplete observable. Subscribe to this object to get updates to
|
||||
* matching autocomplete results as the autocompleteSearchText property is
|
||||
* changed
|
||||
*/
|
||||
get autocompleteOptions() {
|
||||
return this._searchText.pipe(
|
||||
map((txt) => txt.replace("name:", "")),
|
||||
filter((txt) => txt.length),
|
||||
debounceTime(this.debounceInterval),
|
||||
distinctUntilChanged(),
|
||||
switchMap((txt) => this.autocomplete(txt))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this property to start process of retrieving autocomplete results.
|
||||
*/
|
||||
set autocompleteSearchText(txt) {
|
||||
this._searchText.next(txt);
|
||||
}
|
||||
|
||||
async save(tag) {
|
||||
return createTag(tag);
|
||||
}
|
||||
|
||||
async delete(tag) {
|
||||
return createTag(tag);
|
||||
}
|
||||
|
||||
async autocomplete(txt) {
|
||||
return [txt].map(createTag);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { createTag } from "./model";
|
||||
import { TagService } from "./tagService";
|
||||
|
||||
// Some parts of the application save tags by saving model so that
|
||||
// it will update other parts of the app, which is a rudimentary way
|
||||
// of recreating reactivity, so I expect this will probably die soon
|
||||
|
||||
export class BackboneTagService extends TagService {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.model = props.model;
|
||||
}
|
||||
|
||||
async save(rawTag) {
|
||||
const tag = createTag(rawTag);
|
||||
if (!tag.valid) {
|
||||
throw new Error("Invalid tag");
|
||||
}
|
||||
|
||||
// update model
|
||||
let tags = new Set(this.model.attributes.tags);
|
||||
tags.add(tag.text);
|
||||
tags = Array.from(tags);
|
||||
|
||||
await this.model.save({ tags });
|
||||
return tag;
|
||||
}
|
||||
|
||||
async delete(rawTag) {
|
||||
const tag = createTag(rawTag);
|
||||
|
||||
let tags = new Set(this.model.attributes.tags);
|
||||
tags.delete(tag.text);
|
||||
tags = Array.from(tags);
|
||||
|
||||
await this.model.save({ tags });
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,4 @@
|
||||
// Low level stateless UI component
|
||||
export { default as StatelessTags } from "./StatelessTags";
|
||||
|
||||
// Implements data storage logic and click event handling
|
||||
// This is usually what you'll want to use.
|
||||
export { default as Tags } from "./Tags";
|
||||
|
||||
// functions for mounting the tag editor in non-Vue environments
|
||||
export { mountMakoTags, mountModelTags } from "./mounts";
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
/**
|
||||
* These functions are for mounting the tag display/editor in non-Vue
|
||||
* environments such as the existing python scripts and Backbone views.
|
||||
*/
|
||||
|
||||
import { mountVueComponent } from "utils/mountVueComponent";
|
||||
|
||||
import { BackboneTagService } from "./backboneTagService";
|
||||
import Tags from "./Tags";
|
||||
import { TagService } from "./tagService";
|
||||
|
||||
/**
|
||||
* General mount function for the tags that were previously rendered
|
||||
* by the tagging_common.mako file
|
||||
*/
|
||||
export const mountMakoTags = (options = {}, el) => {
|
||||
const { id, itemClass, tags = [], disabled = false, context = "unspecified" } = options;
|
||||
|
||||
const propData = {
|
||||
storeKey: `${itemClass}-${id}`,
|
||||
tagService: new TagService({ id, itemClass, context }),
|
||||
tags,
|
||||
disabled,
|
||||
};
|
||||
|
||||
const fn = mountVueComponent(Tags);
|
||||
const vm = fn(propData, el);
|
||||
vm.$on("tag-click", makoClickHandler(options, vm));
|
||||
return vm;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a click handler for the tags
|
||||
*
|
||||
* @param {object} options Passed options from mount fn
|
||||
*/
|
||||
const makoClickHandler = (options, vm) =>
|
||||
function (tag) {
|
||||
if (!tag) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error("MAKO Tag click handler deprecated");
|
||||
};
|
||||
|
||||
/**
|
||||
* Mount function when a backbone model is provided.
|
||||
*/
|
||||
export const mountModelTags = (options = {}, el) => {
|
||||
const { model, disabled = false, context = "unspecified" } = options;
|
||||
|
||||
if (!model) {
|
||||
console.warn("Missing model in mountModelTags");
|
||||
return;
|
||||
}
|
||||
|
||||
const { id, model_class: itemClass, tags = [] } = model.attributes;
|
||||
|
||||
const propData = {
|
||||
storeKey: `${itemClass}-${id}`,
|
||||
tagService: new BackboneTagService({ id, itemClass, context, model }),
|
||||
tags,
|
||||
disabled,
|
||||
};
|
||||
|
||||
const fn = mountVueComponent(Tags);
|
||||
return fn(propData, el);
|
||||
};
|
||||
@@ -1,118 +0,0 @@
|
||||
/**
|
||||
* 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 the associated python endpoint to a legit json REST service
|
||||
*/
|
||||
import axios from "axios";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { Subject } from "rxjs";
|
||||
import { debounceTime, distinctUntilChanged, filter, map, switchMap } from "rxjs/operators";
|
||||
|
||||
import { createTag } from "./model";
|
||||
|
||||
export class TagService {
|
||||
constructor({ id, itemClass, context, debounceInterval = 150 }) {
|
||||
this.id = id;
|
||||
this.itemClass = itemClass;
|
||||
this.context = context;
|
||||
this.debounceInterval = debounceInterval;
|
||||
|
||||
// Buffer for autocomplete text changes
|
||||
this._searchText = new Subject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Autocomplete observable. Subscribe to this object to get updates to
|
||||
* matching autocomplete results as the autocompleteSearchText property is
|
||||
* changed
|
||||
*/
|
||||
get autocompleteOptions() {
|
||||
return this._searchText.pipe(
|
||||
map((txt) => txt.replace("name:", "")),
|
||||
filter((txt) => txt.length),
|
||||
debounceTime(this.debounceInterval),
|
||||
distinctUntilChanged(),
|
||||
switchMap((txt) => this.autocomplete(txt))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this property to start process of retrieving autocomplete results.
|
||||
*/
|
||||
set autocompleteSearchText(txt) {
|
||||
this._searchText.next(txt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save tag, input can be text string or tag model
|
||||
* @param {string|Tag} tag
|
||||
* @returns Promise yielding new tag model
|
||||
*/
|
||||
async save(rawTag) {
|
||||
const { id, itemClass, context } = this;
|
||||
const tag = createTag(rawTag);
|
||||
if (!tag.valid) {
|
||||
throw new Error("Invalid tag");
|
||||
}
|
||||
const url = `${getAppRoot()}tag/add_tag_async`;
|
||||
const config = {
|
||||
params: { item_id: id, item_class: itemClass, context: context, new_tag: tag.text },
|
||||
};
|
||||
const response = await axios.get(url, config);
|
||||
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 delete(rawTag) {
|
||||
const { id, itemClass, context } = this;
|
||||
const tag = createTag(rawTag);
|
||||
const url = `${getAppRoot()}tag/remove_tag_async`;
|
||||
const config = { params: { item_id: id, item_class: itemClass, context: context, tag_name: tag.text } };
|
||||
const response = await axios.get(url, config);
|
||||
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 autocomplete(searchText) {
|
||||
const { id, itemClass } = this;
|
||||
const url = `${getAppRoot()}tag/tag_autocomplete_data`;
|
||||
const config = { params: { item_id: id, item_class: itemClass, q: searchText } };
|
||||
const response = await axios.get(url, config);
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Unable to retrieve autocomplete tags for search string: ${searchText}`);
|
||||
}
|
||||
return parseAutocompleteResults(response.data).map(createTag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function 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");
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import axios from "axios";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
|
||||
import { createTag } from "./model";
|
||||
import { TagService } from "./tagService";
|
||||
import autocompleteResponse from "./testData/autocompleteResponse.txt";
|
||||
|
||||
describe("Tags/tagService.js", () => {
|
||||
const axiosMock = new MockAdapter(axios);
|
||||
const svcParams = {
|
||||
id: 123,
|
||||
itemClass: "fooClass",
|
||||
context: "",
|
||||
debounceInterval: 50, // shorter value than default for unit tests
|
||||
};
|
||||
const testLabel = "fo0bar123";
|
||||
const expectedParams = {
|
||||
item_id: 123,
|
||||
item_class: "fooClass",
|
||||
context: "",
|
||||
};
|
||||
|
||||
const svc = new TagService(svcParams);
|
||||
beforeEach(() => {
|
||||
axiosMock.onAny().reply(200, autocompleteResponse);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
});
|
||||
|
||||
describe("save", () => {
|
||||
const testTag = createTag(testLabel);
|
||||
|
||||
const expectedSaveUrl = `/tag/add_tag_async`;
|
||||
|
||||
it("should save a string tag", async () => {
|
||||
const savedTag = await svc.save(testLabel);
|
||||
expect(savedTag.text).toBe(testLabel);
|
||||
expect(axiosMock.history.get[0].url).toBe(expectedSaveUrl);
|
||||
expect(axiosMock.history.get[0].params).toEqual({ ...expectedParams, new_tag: testLabel });
|
||||
});
|
||||
|
||||
it("should save an object tag", async () => {
|
||||
const savedTag = await svc.save(testTag);
|
||||
expect(savedTag.text).toBe(testLabel);
|
||||
expect(axiosMock.history.get[0].url).toBe(expectedSaveUrl);
|
||||
expect(axiosMock.history.get[0].params).toEqual({ ...expectedParams, new_tag: testLabel });
|
||||
});
|
||||
|
||||
// TODO: test error conditions
|
||||
});
|
||||
describe("delete", () => {
|
||||
const testTag = createTag(testLabel);
|
||||
|
||||
const expectedDeleteUrl = `/tag/remove_tag_async`;
|
||||
|
||||
it("should delete a text tag", async () => {
|
||||
const result = await svc.delete(testTag);
|
||||
expect(result).toBeTruthy();
|
||||
expect(axiosMock.history.get[0].url).toBe(expectedDeleteUrl);
|
||||
expect(axiosMock.history.get[0].params).toEqual({ ...expectedParams, tag_name: testLabel });
|
||||
});
|
||||
|
||||
it("should delete an object tag", async () => {
|
||||
const result = await svc.delete(testTag);
|
||||
expect(result).toBeTruthy();
|
||||
expect(axiosMock.history.get[0].url).toBe(expectedDeleteUrl);
|
||||
expect(axiosMock.history.get[0].params).toEqual({ ...expectedParams, tag_name: testLabel });
|
||||
});
|
||||
|
||||
// TODO: test error conditions
|
||||
});
|
||||
|
||||
describe("autocomplete", () => {
|
||||
const searchString = "foo";
|
||||
const expectedSearchUrl = `/tag/tag_autocomplete_data`;
|
||||
|
||||
const checkAutocompleteResult = (result) => {
|
||||
expect(result).toBeTruthy();
|
||||
expect(result instanceof Array).toBeTruthy();
|
||||
expect(result.length == 2).toBeTruthy();
|
||||
expect(result[0] instanceof Object).toBeTruthy();
|
||||
expect((result[0].text = "abc")).toBeTruthy();
|
||||
};
|
||||
|
||||
// straight ajax request, unused in practice, but it's easier to
|
||||
// test this call if we just expose it
|
||||
const { ...searchParams } = expectedParams;
|
||||
delete searchParams.context;
|
||||
|
||||
it("ajax call should return tag objects", async () => {
|
||||
const result = await svc.autocomplete(searchString);
|
||||
expect(axiosMock.history.get[0].url).toBe(expectedSearchUrl);
|
||||
expect(axiosMock.history.get[0].params).toEqual({ ...searchParams, q: searchString });
|
||||
checkAutocompleteResult(result);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
#Header|Your Tags
|
||||
abc|abc
|
||||
def|def
|
||||
Reference in New Issue
Block a user