Unverified Commit 4d6ce901 authored by mvdbeek's avatar mvdbeek
Browse files

Merge branch 'release_24.2' into dev

parents 9bdeea09 2e684168
Loading
Loading
Loading
Loading
+27 −24
Original line number Diff line number Diff line
import "tests/jest/mockHelpPopovers";
import "@/composables/__mocks__/filter";

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

import MountTarget from "./FormData.vue";

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

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

@@ -51,7 +54,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 () => {
@@ -74,11 +77,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);
@@ -86,7 +89,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 () => {
@@ -128,8 +131,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,
@@ -176,9 +179,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,
@@ -226,11 +229,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,
@@ -261,7 +264,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 () => {
@@ -274,7 +277,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 () => {
@@ -291,7 +294,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 () => {
@@ -309,7 +312,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 () => {
@@ -327,7 +330,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 () => {
@@ -513,22 +516,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>
+2 −1
Original line number Diff line number Diff line
@@ -91,6 +91,7 @@ import { waitOnJob } from "components/JobStates/wait";
import LoadingSpan from "components/LoadingSpan";
import { getAppRoot } from "onload/loadConfig";
import { errorMessageAsString } from "utils/simple-error";
import { capitalizeFirstLetter } from "utils/strings";
import Vue, { ref, watch } from "vue";

import { fetchFileSources } from "@/api/remoteFiles";
@@ -165,7 +166,7 @@ export default {
            return this.invocationImport ? "invocation" : "history";
        },
        identifierTextCapitalized() {
            return this.identifierText.charAt(0).toUpperCase() + this.identifierText.slice(1);
            return capitalizeFirstLetter(this.identifierText);
        },
        identifierTextPlural() {
            return this.invocationImport ? "invocations" : "histories";
+1 −4
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@ import { computed } from "vue";
import type { SharedItemNotification } from "@/api/notifications";
import { useNotificationsStore } from "@/stores/notificationsStore";
import { absPath } from "@/utils/redirect";
import { capitalizeFirstLetter } from "@/utils/strings";

import NotificationActions from "@/components/Notifications/NotificationActions.vue";

@@ -21,10 +22,6 @@ const props = defineProps<Props>();

const notificationsStore = useNotificationsStore();

function capitalizeFirstLetter(string: string) {
    return string.charAt(0).toUpperCase() + string.slice(1);
}

const content = computed(() => props.notification.content);

const sharedItemType = computed(() => {
+7 −1
Original line number Diff line number Diff line
@@ -46,7 +46,8 @@
                title="Disconnect External Identity"
                class="d-block mt-3"
                @click="onDisconnect(item)">
                Disconnect {{ item.provider.charAt(0).toUpperCase() + item.provider.slice(1) }} - {{ item.email }}
                Disconnect {{ capitalizeAsTitle(item.provider_label) }} -
                {{ item.email }}
            </b-button>

            <b-modal
@@ -98,6 +99,8 @@ import { sanitize } from "dompurify";
import { userLogout } from "utils/logout";
import Vue from "vue";

import { capitalizeFirstLetter } from "@/utils/strings";

import svc from "./service";

import ExternalLogin from "components/User/ExternalIdentities/ExternalLogin.vue";
@@ -156,6 +159,9 @@ export default {
        Toast.success(notificationMessage);
    },
    methods: {
        capitalizeAsTitle(str) {
            return capitalizeFirstLetter(str);
        },
        loadIdentities() {
            this.loading = true;
            svc.getIdentityProviders()
Loading