Remove legacy Tags component

This commit is contained in:
davelopez
2024-01-24 11:34:42 +01:00
parent 8524cafe6a
commit 5a4cb9accd
10 changed files with 0 additions and 625 deletions
-2
View File
@@ -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";
-132
View File
@@ -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;
});
});
});
-99
View File
@@ -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;
}
}
-5
View File
@@ -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";
-68
View File
@@ -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);
};
-118
View File
@@ -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