Unverified Commit f06b0d7f authored by Laila Los's avatar Laila Los Committed by GitHub
Browse files

Merge pull request #18918 from davelopez/24.1_fix_missing_tags_in_selector

[24.1] Fix display tags in FormSelect when available
parents dda0e9b4 484dfc52
Loading
Loading
Loading
Loading
+28 −24
Original line number Diff line number Diff line
import "@/composables/__mocks__/filter";

import { createTestingPinia } from "@pinia/testing";
import { mount } from "@vue/test-utils";
import { PiniaVuePlugin } from "pinia";
@@ -9,6 +11,8 @@ import { useEventStore } from "@/stores/eventStore";

import MountTarget from "./FormData.vue";

jest.mock("@/composables/filter");

const localVue = getLocalVue();
localVue.use(PiniaVuePlugin);

@@ -49,7 +53,7 @@ const defaultOptions = {
};

const SELECT_OPTIONS = ".multiselect__element";
const SELECTED_VALUE = ".multiselect__option--selected span";
const SELECTED_VALUE = ".multiselect__option--selected";

describe("FormData", () => {
    it("regular data", async () => {
@@ -72,11 +76,11 @@ describe("FormData", () => {
        expect(options.at(0).classes()).toContain("active");
        expect(options.at(0).attributes("title")).toBe("Single dataset");
        expect(wrapper.emitted().input[0][0]).toEqual(value_0);
        expect(wrapper.find(SELECTED_VALUE).text()).toEqual("dceName4 (as dataset)");
        expect(wrapper.find(SELECTED_VALUE).text()).toContain("dceName4 (as dataset)");
        await wrapper.setProps({ value: value_0 });
        expect(wrapper.emitted().input.length).toEqual(1);
        await wrapper.setProps({ value: { values: [{ id: "hda2", src: "hda" }] } });
        expect(wrapper.find(SELECTED_VALUE).text()).toEqual("2: hdaName2");
        expect(wrapper.find(SELECTED_VALUE).text()).toContain("2: hdaName2");
        expect(wrapper.emitted().input.length).toEqual(1);
        const elements_0 = wrapper.findAll(SELECT_OPTIONS);
        expect(elements_0.length).toEqual(6);
@@ -84,7 +88,7 @@ describe("FormData", () => {
        expect(wrapper.emitted().input.length).toEqual(2);
        expect(wrapper.emitted().input[1][0]).toEqual(value_1);
        await wrapper.setProps({ value: value_1 });
        expect(wrapper.find(SELECTED_VALUE).text()).toEqual("4: hdaName4");
        expect(wrapper.find(SELECTED_VALUE).text()).toContain("4: hdaName4");
    });

    it("optional dataset", async () => {
@@ -126,8 +130,8 @@ describe("FormData", () => {
        expect(wrapper.emitted().input.length).toEqual(1);
        const selectedValues = wrapper.findAll(SELECTED_VALUE);
        expect(selectedValues.length).toBe(2);
        expect(selectedValues.at(0).text()).toBe("3: hdaName3");
        expect(selectedValues.at(1).text()).toBe("2: hdaName2");
        expect(selectedValues.at(0).text()).toContain("3: hdaName3");
        expect(selectedValues.at(1).text()).toContain("2: hdaName2");
        const value_0 = {
            batch: false,
            product: false,
@@ -174,9 +178,9 @@ describe("FormData", () => {
        const selectedValues = wrapper.findAll(SELECTED_VALUE);
        expect(selectedValues.length).toBe(3);
        // the values in the multiselect are sorted by hid DESC
        expect(selectedValues.at(0).text()).toBe("3: hdaName3");
        expect(selectedValues.at(1).text()).toBe("2: hdaName2");
        expect(selectedValues.at(2).text()).toBe("1: hdaName1");
        expect(selectedValues.at(0).text()).toContain("3: hdaName3");
        expect(selectedValues.at(1).text()).toContain("2: hdaName2");
        expect(selectedValues.at(2).text()).toContain("1: hdaName1");
        await selectedValues.at(0).trigger("click");
        const value_sorted = {
            batch: false,
@@ -224,11 +228,11 @@ describe("FormData", () => {
        expect(selectedValues.length).toBe(5);
        // when dces are mixed in their values are shown first and are
        // ordered by id descending
        expect(selectedValues.at(0).text()).toBe("dceName4 (as dataset)");
        expect(selectedValues.at(1).text()).toBe("dceName3 (as dataset)");
        expect(selectedValues.at(2).text()).toBe("dceName2 (as dataset)");
        expect(selectedValues.at(3).text()).toBe("2: hdaName2");
        expect(selectedValues.at(4).text()).toBe("1: hdaName1");
        expect(selectedValues.at(0).text()).toContain("dceName4 (as dataset)");
        expect(selectedValues.at(1).text()).toContain("dceName3 (as dataset)");
        expect(selectedValues.at(2).text()).toContain("dceName2 (as dataset)");
        expect(selectedValues.at(3).text()).toContain("2: hdaName2");
        expect(selectedValues.at(4).text()).toContain("1: hdaName1");
        await selectedValues.at(0).trigger("click");
        const value_sorted = {
            batch: false,
@@ -259,7 +263,7 @@ describe("FormData", () => {
        expect(wrapper.emitted().input.length).toEqual(1);
        const selectedValues = wrapper.findAll(SELECTED_VALUE);
        expect(selectedValues.length).toBe(1);
        expect(selectedValues.at(0).text()).toBe("dceName1 (as dataset)");
        expect(selectedValues.at(0).text()).toContain("dceName1 (as dataset)");
    });

    it("dataset collection element as hdca without map_over_type", async () => {
@@ -272,7 +276,7 @@ describe("FormData", () => {
        await wrapper.vm.$nextTick();
        const selectedValues = wrapper.findAll(SELECTED_VALUE);
        expect(selectedValues.length).toBe(1);
        expect(selectedValues.at(0).text()).toBe("dceName2 (as dataset collection)");
        expect(selectedValues.at(0).text()).toContain("dceName2 (as dataset collection)");
    });

    it("dataset collection element as hdca mapped to batch field", async () => {
@@ -289,7 +293,7 @@ describe("FormData", () => {
        await wrapper.vm.$nextTick();
        const selectedValues = wrapper.findAll(SELECTED_VALUE);
        expect(selectedValues.length).toBe(1);
        expect(selectedValues.at(0).text()).toBe("dceName3 (as dataset collection)");
        expect(selectedValues.at(0).text()).toContain("dceName3 (as dataset collection)");
    });

    it("dataset collection element as hdca mapped to non-batch field", async () => {
@@ -307,7 +311,7 @@ describe("FormData", () => {
        await wrapper.vm.$nextTick();
        const selectedValues = wrapper.findAll(SELECTED_VALUE);
        expect(selectedValues.length).toBe(1);
        expect(selectedValues.at(0).text()).toBe("dceName3 (as dataset collection)");
        expect(selectedValues.at(0).text()).toContain("dceName3 (as dataset collection)");
    });

    it("dataset collection mapped to non-batch field", async () => {
@@ -325,7 +329,7 @@ describe("FormData", () => {
        await wrapper.vm.$nextTick();
        const selectedValues = wrapper.findAll(SELECTED_VALUE);
        expect(selectedValues.length).toBe(1);
        expect(selectedValues.at(0).text()).toBe("5: hdcaName5");
        expect(selectedValues.at(0).text()).toContain("5: hdcaName5");
    });

    it("multiple dataset collection elements (as hdas)", async () => {
@@ -463,22 +467,22 @@ describe("FormData", () => {
        });
        const select_0 = wrapper_0.findAll(SELECT_OPTIONS);
        expect(select_0.length).toBe(4);
        expect(select_0.at(2).text()).toBe("2: hdaName2");
        expect(select_0.at(3).text()).toBe("1: hdaName1");
        expect(select_0.at(2).text()).toContain("2: hdaName2");
        expect(select_0.at(3).text()).toContain("1: hdaName1");
        const wrapper_1 = createTarget({
            tag: "tag2",
            options: defaultOptions,
        });
        const select_1 = wrapper_1.findAll(SELECT_OPTIONS);
        expect(select_1.length).toBe(4);
        expect(select_1.at(2).text()).toBe("3: hdaName3");
        expect(select_1.at(3).text()).toBe("2: hdaName2");
        expect(select_1.at(2).text()).toContain("3: hdaName3");
        expect(select_1.at(3).text()).toContain("2: hdaName2");
        const wrapper_2 = createTarget({
            tag: "tag3",
            options: defaultOptions,
        });
        const select_2 = wrapper_2.findAll(SELECT_OPTIONS);
        expect(select_2.length).toBe(3);
        expect(select_2.at(2).text()).toBe("3: hdaName3");
        expect(select_2.at(2).text()).toContain("3: hdaName3");
    });
});
+32 −6
Original line number Diff line number Diff line
@@ -2,17 +2,21 @@
import { library } from "@fortawesome/fontawesome-svg-core";
import { faCheckSquare, faSquare } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { computed, type ComputedRef, onMounted, type PropType, watch } from "vue";
import { computed, type ComputedRef, onMounted, type PropType, ref, watch } from "vue";
import Multiselect from "vue-multiselect";

import { useFilterObjectArray } from "@/composables/filter";
import { useMultiselect } from "@/composables/useMultiselect";
import { uid } from "@/utils/utils";

import StatelessTags from "@/components/TagsMultiselect/StatelessTags.vue";

library.add(faCheckSquare, faSquare);

const { ariaExpanded, onOpen, onClose } = useMultiselect();

type SelectValue = Record<string, unknown> | string | number | null;
type ValueWithTags = SelectValue & { tags: string[] };

interface SelectOption {
    label: string;
@@ -51,19 +55,22 @@ const emit = defineEmits<{
    (e: "input", value: SelectValue | Array<SelectValue>): void;
}>();

const filter = ref("");
const filteredOptions = useFilterObjectArray(() => props.options, filter, ["label", ["value", "tags"]]);

/**
 * When there are more options than this, push selected options to the end
 */
const optionReorderThreshold = 8;

const reorderedOptions = computed(() => {
    if (props.options.length <= optionReorderThreshold) {
        return props.options;
    if (filteredOptions.value.length <= optionReorderThreshold) {
        return filteredOptions.value;
    } else {
        const selectedOptions: SelectOption[] = [];
        const unselectedOptions: SelectOption[] = [];

        props.options.forEach((option) => {
        filteredOptions.value.forEach((option) => {
            if (selectedValues.value.includes(option.value)) {
                selectedOptions.push(option);
            } else {
@@ -140,7 +147,9 @@ function setInitialValue(): void {
 */
watch(
    () => props.options,
    () => setInitialValue()
    () => {
        setInitialValue();
    }
);

/**
@@ -149,6 +158,14 @@ watch(
onMounted(() => {
    setInitialValue();
});

function isValueWithTags(item: SelectValue): item is ValueWithTags {
    return item !== null && typeof item === "object" && (item as ValueWithTags).tags !== undefined;
}

function onSearchChange(search: string): void {
    filter.value = search;
}
</script>

<template>
@@ -169,11 +186,20 @@ onMounted(() => {
            :selected-label="selectedLabel"
            :select-label="null"
            track-by="value"
            :internal-search="false"
            @search-change="onSearchChange"
            @open="onOpen"
            @close="onClose">
            <template v-slot:option="{ option }">
                <div class="d-flex align-items-center justify-content-between">
                    <div>
                        <span>{{ option.label }}</span>
                        <StatelessTags
                            v-if="isValueWithTags(option.value)"
                            class="tags mt-2"
                            :value="option.value.tags"
                            disabled />
                    </div>
                    <FontAwesomeIcon v-if="selectedValues.includes(option.value)" :icon="faCheckSquare" />
                    <FontAwesomeIcon v-else :icon="faSquare" />
                </div>
+13 −0
Original line number Diff line number Diff line
import { toValue } from "@vueuse/core";
import { computed, Ref } from "vue";

import type { useFilterObjectArray as UseFilterObjectArray } from "@/composables/filter";

jest.mock("@/composables/filter", () => ({
    useFilterObjectArray,
}));

export const useFilterObjectArray: typeof UseFilterObjectArray = (array): Ref<any[]> => {
    console.debug("USING MOCKED useFilterObjectArray");
    return computed(() => toValue(array));
};
+2 −2
Original line number Diff line number Diff line
@@ -6,10 +6,10 @@ import type { Ref } from "vue";
 * All parameters can optionally be refs.
 * @param array array of objects to filter
 * @param filter string to filter by
 * @param objectFields string array of fields to filter by on each object
 * @param objectFields string array of fields to filter by on each object. To reach nested fields, use an array of strings (e.g. `["nested", "field"]`)
 */
export declare function useFilterObjectArray<O extends object, K extends keyof O>(
    array: MaybeRefOrGetter<Array<O>>,
    filter: MaybeRefOrGetter<string>,
    objectFields: MaybeRefOrGetter<Array<K>>
    objectFields: MaybeRefOrGetter<Array<K | string[]>>
): Ref<O[]>;
+18 −6
Original line number Diff line number Diff line
export function runFilter<O extends object, K extends keyof O>(f: string, arr: O[], fields: K[]) {
export function runFilter<O extends object, K extends keyof O>(f: string, arr: O[], fields: (K | string[])[]) {
    if (f === "") {
        return arr;
    } else {
        return arr.filter((obj) => {
            const lowerCaseFilter = f.toLocaleLowerCase();
            for (const field of fields) {
                const val = obj[field];
                let val: unknown;

                if (typeof field === "string") {
                    val = obj[field];
                } else if (Array.isArray(field)) {
                    val = field.reduce((acc: unknown, curr: string) => {
                        if (acc && typeof acc === "object") {
                            return (acc as Record<string, unknown>)[curr];
                        }
                        return undefined;
                    }, obj);
                }

                if (typeof val === "string") {
                    if (val.toLowerCase().includes(f.toLocaleLowerCase())) {
                    if (val.toLowerCase().includes(lowerCaseFilter)) {
                        return true;
                    }
                } else if (Array.isArray(val)) {
                    if (val.includes(f)) {
                        return true;
                    }
                    return val.find((v) => {
                        return typeof v === "string" ? v.toLowerCase().includes(lowerCaseFilter) : false;
                    });
                }
            }