Loading client/src/components/Form/Elements/FormData/FormData.test.js +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"; Loading @@ -9,6 +11,8 @@ import { useEventStore } from "@/stores/eventStore"; import MountTarget from "./FormData.vue"; jest.mock("@/composables/filter"); const localVue = getLocalVue(); localVue.use(PiniaVuePlugin); Loading Loading @@ -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 () => { Loading @@ -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); Loading @@ -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 () => { Loading Loading @@ -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, Loading Loading @@ -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, Loading Loading @@ -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, Loading Loading @@ -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 () => { Loading @@ -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 () => { Loading @@ -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 () => { Loading @@ -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 () => { Loading @@ -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 () => { Loading Loading @@ -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"); }); }); client/src/components/Form/Elements/FormSelect.vue +32 −6 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 { Loading Loading @@ -140,7 +147,9 @@ function setInitialValue(): void { */ watch( () => props.options, () => setInitialValue() () => { setInitialValue(); } ); /** Loading @@ -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> Loading @@ -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> Loading client/src/composables/__mocks__/filter.ts 0 → 100644 +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)); }; client/src/composables/filter/filter.d.ts +2 −2 Original line number Diff line number Diff line Loading @@ -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[]>; client/src/composables/filter/filterFunction.ts +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; }); } } Loading Loading
client/src/components/Form/Elements/FormData/FormData.test.js +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"; Loading @@ -9,6 +11,8 @@ import { useEventStore } from "@/stores/eventStore"; import MountTarget from "./FormData.vue"; jest.mock("@/composables/filter"); const localVue = getLocalVue(); localVue.use(PiniaVuePlugin); Loading Loading @@ -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 () => { Loading @@ -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); Loading @@ -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 () => { Loading Loading @@ -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, Loading Loading @@ -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, Loading Loading @@ -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, Loading Loading @@ -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 () => { Loading @@ -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 () => { Loading @@ -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 () => { Loading @@ -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 () => { Loading @@ -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 () => { Loading Loading @@ -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"); }); });
client/src/components/Form/Elements/FormSelect.vue +32 −6 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 { Loading Loading @@ -140,7 +147,9 @@ function setInitialValue(): void { */ watch( () => props.options, () => setInitialValue() () => { setInitialValue(); } ); /** Loading @@ -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> Loading @@ -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> Loading
client/src/composables/__mocks__/filter.ts 0 → 100644 +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)); };
client/src/composables/filter/filter.d.ts +2 −2 Original line number Diff line number Diff line Loading @@ -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[]>;
client/src/composables/filter/filterFunction.ts +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; }); } } Loading