From 5a4cb9accdfe157b56237ea4fbefe6c2d0212ad7 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Wed, 24 Jan 2024 11:33:33 +0100 Subject: [PATCH] Remove legacy Tags component --- client/src/bundleEntries.js | 2 - client/src/components/Tags/Tags.test.js | 132 ------------------ client/src/components/Tags/Tags.vue | 99 ------------- .../components/Tags/__mocks__/tagService.js | 60 -------- .../src/components/Tags/backboneTagService.js | 39 ------ client/src/components/Tags/index.js | 5 - client/src/components/Tags/mounts.js | 68 --------- client/src/components/Tags/tagService.js | 118 ---------------- client/src/components/Tags/tagService.test.js | 99 ------------- .../Tags/testData/autocompleteResponse.txt | 3 - 10 files changed, 625 deletions(-) delete mode 100644 client/src/components/Tags/Tags.test.js delete mode 100644 client/src/components/Tags/Tags.vue delete mode 100644 client/src/components/Tags/__mocks__/tagService.js delete mode 100644 client/src/components/Tags/backboneTagService.js delete mode 100644 client/src/components/Tags/mounts.js delete mode 100644 client/src/components/Tags/tagService.js delete mode 100644 client/src/components/Tags/tagService.test.js delete mode 100644 client/src/components/Tags/testData/autocompleteResponse.txt diff --git a/client/src/bundleEntries.js b/client/src/bundleEntries.js index d25a2e7a930..acfb8027b07 100644 --- a/client/src/bundleEntries.js +++ b/client/src/bundleEntries.js @@ -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"; diff --git a/client/src/components/Tags/Tags.test.js b/client/src/components/Tags/Tags.test.js deleted file mode 100644 index 3827c2f72ae..00000000000 --- a/client/src/components/Tags/Tags.test.js +++ /dev/null @@ -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; - }); - }); -}); diff --git a/client/src/components/Tags/Tags.vue b/client/src/components/Tags/Tags.vue deleted file mode 100644 index b357f018fa2..00000000000 --- a/client/src/components/Tags/Tags.vue +++ /dev/null @@ -1,99 +0,0 @@ - - - diff --git a/client/src/components/Tags/__mocks__/tagService.js b/client/src/components/Tags/__mocks__/tagService.js deleted file mode 100644 index acbff9490fa..00000000000 --- a/client/src/components/Tags/__mocks__/tagService.js +++ /dev/null @@ -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); - } -} diff --git a/client/src/components/Tags/backboneTagService.js b/client/src/components/Tags/backboneTagService.js deleted file mode 100644 index 60912fa3d62..00000000000 --- a/client/src/components/Tags/backboneTagService.js +++ /dev/null @@ -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; - } -} diff --git a/client/src/components/Tags/index.js b/client/src/components/Tags/index.js index d508d536d27..3adeb8c36c4 100644 --- a/client/src/components/Tags/index.js +++ b/client/src/components/Tags/index.js @@ -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"; diff --git a/client/src/components/Tags/mounts.js b/client/src/components/Tags/mounts.js deleted file mode 100644 index 49668e5f09f..00000000000 --- a/client/src/components/Tags/mounts.js +++ /dev/null @@ -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); -}; diff --git a/client/src/components/Tags/tagService.js b/client/src/components/Tags/tagService.js deleted file mode 100644 index ab79a244757..00000000000 --- a/client/src/components/Tags/tagService.js +++ /dev/null @@ -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"); -} diff --git a/client/src/components/Tags/tagService.test.js b/client/src/components/Tags/tagService.test.js deleted file mode 100644 index 5e73089eab3..00000000000 --- a/client/src/components/Tags/tagService.test.js +++ /dev/null @@ -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); - }); - }); -}); diff --git a/client/src/components/Tags/testData/autocompleteResponse.txt b/client/src/components/Tags/testData/autocompleteResponse.txt deleted file mode 100644 index 7d8b85f44af..00000000000 --- a/client/src/components/Tags/testData/autocompleteResponse.txt +++ /dev/null @@ -1,3 +0,0 @@ -#Header|Your Tags -abc|abc -def|def \ No newline at end of file