#7136 - Enable paginate event in VnSelectFilter #255

Closed
jsegarra wants to merge 86 commits from 7136_vnselectFilter_paginate into dev
6 changed files with 247 additions and 101 deletions

View File

@ -1,7 +1,8 @@
<script setup>
import { ref, toRefs, computed, watch, onMounted, useAttrs } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'src/components/FetchData.vue';
import { useArrayData } from 'src/composables/useArrayData';
// import FetchData from 'src/components/FetchData.vue';
import { useValidator } from 'src/composables/useValidator';
const emit = defineEmits(['update:modelValue', 'update:options', 'remove']);
@ -22,6 +23,10 @@ const $props = defineProps({
type: String,
default: 'id',
},
dataKey: {
type: String,
default: null,
},
optionFilter: {
type: String,
default: null,
@ -60,7 +65,7 @@ const $props = defineProps({
},
where: {
type: Object,
default: null,
default: () => {},
},
sortBy: {
type: String,
@ -94,18 +99,20 @@ const { t } = useI18n();
const mixinRules = [requiredFieldRule, ...($attrs.rules ?? [])];
const { optionLabel, optionValue, optionFilter, optionFilterValue, options, modelValue } =
toRefs($props);
const myOptions = ref([]);
const myOptionsOriginal = ref([]);
const myOptions = ref($props.options);
const myOptionsOriginal = ref($props.options);
const vnSelectRef = ref();
const isLoading = ref(false);
const dataRef = ref();
const lastVal = ref();
const useURL = computed(() => $props.url);
const noOneText = t('globals.noOne');
const noOneOpt = ref({
[optionValue.value]: false,
[optionLabel.value]: noOneText,
});
const value = computed({
const selectValue = computed({
get() {
return $props.modelValue;
},
@ -115,6 +122,16 @@ const value = computed({
},
});
const arrayDataKey =
$props.dataKey ?? ($props.url?.length > 0 ? $props.url : $attrs.name ?? $attrs.label);
const arrayData = useArrayData(arrayDataKey, { url: $props.url });
onMounted(async () => {
if ($props.focusOnMount) setTimeout(() => vnSelectRef.value.showPopup(), 300);
setOptions(options.value);
if (useURL.value && $props.modelValue) await fetchFilter($props.modelValue);
});
watch(options, (newValue) => {
setOptions(newValue);
});
@ -126,21 +143,9 @@ watch(modelValue, async (newValue) => {
if ($props.noOne) myOptions.value.unshift(noOneOpt.value);
});
onMounted(() => {
setOptions(options.value);
if ($props.url && $props.modelValue && !findKeyInOptions())
fetchFilter($props.modelValue);
if ($props.focusOnMount) setTimeout(() => vnSelectRef.value.showPopup(), 300);
});
function findKeyInOptions() {
if (!$props.options) return;
return filter($props.modelValue, $props.options)?.length;
}
function setOptions(data) {
function setOptions(data, append = true) {
myOptions.value = JSON.parse(JSON.stringify(data));
myOptionsOriginal.value = JSON.parse(JSON.stringify(data));
if (append) myOptionsOriginal.value = JSON.parse(JSON.stringify(data));
}
function filter(val, options) {
@ -165,7 +170,7 @@ function filter(val, options) {
}
async function fetchFilter(val) {
if (!$props.url || !dataRef.value) return;
if (!$props.url) return;
const { fields, include, sortBy, limit } = $props;
const key =
@ -188,7 +193,15 @@ async function fetchFilter(val) {
if (fields) fetchOptions.fields = fields;
if (sortBy) fetchOptions.order = sortBy;
return dataRef.value.fetch(fetchOptions);
// return dataRef.value.fetch(fetchOptions);
arrayData.store.userParams = $props.params;
arrayData.store.userFilter = fetchOptions;
arrayData.store.order = fetchOptions.order;
arrayData.store.skip = 0;
arrayData.store.filter.skip = 0;
const { data } = await arrayData.fetch({ append: true, updateRouter: false });
setOptions(data);
return data;
}
async function filterHandler(val, update) {
@ -200,10 +213,7 @@ async function filterHandler(val, update) {
let newOptions;
if (!$props.defaultFilter) return update();
if (
$props.url &&
($props.limit || (!$props.limit && Object.keys(myOptions.value).length === 0))
) {
if (useURL.value) {
newOptions = await fetchFilter(val);
} else newOptions = filter(val, myOptionsOriginal.value);
update(
@ -227,21 +237,39 @@ function nullishToTrue(value) {
}
const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val);
async function onScroll({ to, direction, from, index }) {
const lastIndex = myOptions.value.length - 1;
if (from === 0 && index === 0) return;
if (!useURL.value && !$props.fetchRef) return;
if (direction === 'decrease') return;
if (to === lastIndex && arrayData.store.hasMoreData && !isLoading.value) {
isLoading.value = true;
await arrayData.loadMore();
setOptions(arrayData.store.data);
vnSelectRef.value.scrollTo(lastIndex);
isLoading.value = false;
}
}
</script>
<template>
<FetchData
<!-- <FetchData
ref="dataRef"
:url="$props.url"
@on-fetch="(data) => setOptions(data)"
:where="where || { [optionValue]: value }"
:where="where || { [optionValue]: selectValue }"
:limit="limit"
:sort-by="sortBy"
:fields="fields"
:params="params"
/>
/> -->
<QSelect
v-model="value"
:input-debounce="$attrs.url ? 300 : 0"
:loading="isLoading"
@virtual-scroll="onScroll"
v-model="selectValue"
:options="myOptions"
:option-label="optionLabel"
:option-value="optionValue"
@ -263,16 +291,12 @@ const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val);
<QIcon
v-show="value"
name="close"
@click.stop="
() => {
value = null;
emit('remove');
}
"
@click.stop="value = null"
class="cursor-pointer"
size="xs"
/>
</template>
<template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName">
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
</template>

View File

@ -235,8 +235,12 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
async function loadMore() {
if (!store.hasMoreData) return;
const isCurrentFilter = JSON.parse(store?.currentFilter?.filter ?? '{}');
store.skip = store.limit * store.page;
const limit = isCurrentFilter
? isCurrentFilter.limit ?? store.limit
: store.limit;
store.skip = limit * store.page;
store.page += 1;
await fetch({ append: true });

View File

@ -110,6 +110,7 @@ const exprBuilder = (param, value) => {
:params="{
departmentCodes: ['VT', 'shopping'],
}"
map-options
:fields="['id', 'nickname']"
sort-by="nickname ASC"
option-label="nickname"
@ -117,7 +118,6 @@ const exprBuilder = (param, value) => {
:rules="validate('client.salesPersonFk')"
:expr-builder="exprBuilder"
emit-value
auto-load
>
<template #prepend>
<VnAvatar
@ -153,17 +153,15 @@ const exprBuilder = (param, value) => {
<VnRow>
<VnSelect
url="Clients"
:input-debounce="0"
:label="t('customer.basicData.previousClient')"
:options="clients"
v-model="data.transferorFk"
:fields="['id', 'name']"
:rules="validate('client.transferorFk')"
emit-value
map-options
option-label="name"
option-value="id"
sort-by="name ASC"
v-model="data.transferorFk"
:fields="['id', 'name']"
>
<template #append>
<QIcon name="info" class="cursor-pointer">

View File

@ -1,7 +1,7 @@
<script setup>
import { ref } from 'vue';
// import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
// import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
@ -15,26 +15,15 @@ const props = defineProps({
},
});
const salespersons = ref();
const countries = ref();
const authors = ref();
const departments = ref();
// const salespersons = ref();
// const countries = ref();
// const authors = ref();
// const departments = ref();
</script>
<template>
<FetchData
:filter="{ where: { role: 'salesPerson' } }"
@on-fetch="(data) => (salespersons = data)"
auto-load
url="Workers/activeWithInheritedRole"
/>
<FetchData @on-fetch="(data) => (countries = data)" auto-load url="Countries" />
<FetchData
@on-fetch="(data) => (authors = data)"
auto-load
url="Workers/activeWithInheritedRole"
/>
<FetchData @on-fetch="(data) => (departments = data)" auto-load url="Departments" />
<!-- <FetchData @on-fetch="(data) => (countries = data)" auto-load url="Countries" /> -->
<!-- <FetchData @on-fetch="(data) => (departments = data)" auto-load /> -->
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }">
@ -46,28 +35,9 @@ const departments = ref();
<template #body="{ params, searchFn }">
<QItem class="q-mb-sm">
<VnSelect
:label="t('Client')"
url="Clients"
dense
option-label="name"
option-value="id"
outlined
rounded
emit-value
hide-selected
map-options
v-model="params.clientFk"
use-input
@update:model-value="searchFn()"
/>
</QItem>
<QItem class="q-mb-sm">
<QItemSection v-if="salespersons">
<QItemSection>
<VnSelect
:input-debounce="0"
:label="t('Salesperson')"
:options="salespersons"
:label="t('Client')"
dense
emit-value
hide-selected
@ -77,20 +47,42 @@ const departments = ref();
outlined
rounded
use-input
v-model="params.clientFk"
@update:model-value="searchFn()"
url="Clients"
/>
</QItemSection>
</QItem>
<QItem class="q-mb-sm">
<QItemSection>
<VnSelect
data-key="salesperson"
url="Workers/activeWithInheritedRole"
:input-debounce="0"
:label="t('Salesperson')"
dense
emit-value
hide-selected
map-options
option-label="name"
option-value="id"
:where="{ role: 'salesPerson' }"
option-filter="firstName"
outlined
rounded
use-input
v-model="params.salesPersonFk"
@update:model-value="searchFn()"
/>
</QItemSection>
<QItemSection v-else>
<QSkeleton class="full-width" type="QInput" />
</QItemSection>
</QItem>
<QItem class="q-mb-sm">
<QItemSection v-if="departments">
<QItemSection>
<VnSelect
:input-debounce="0"
:label="t('Departments')"
:options="departments"
url="Departments"
dense
emit-value
hide-selected
@ -104,17 +96,13 @@ const departments = ref();
@update:model-value="searchFn()"
/>
</QItemSection>
<QItemSection v-else>
<QSkeleton class="full-width" type="QInput" />
</QItemSection>
</QItem>
<QItem class="q-mb-sm">
<QItemSection v-if="countries">
<QItemSection>
<VnSelect
:input-debounce="0"
:label="t('Country')"
:options="countries"
url="Countries"
dense
emit-value
hide-selected
@ -128,9 +116,6 @@ const departments = ref();
@update:model-value="searchFn()"
/>
</QItemSection>
<QItemSection v-else>
<QSkeleton class="full-width" type="QInput" />
</QItemSection>
</QItem>
<QItem class="q-mb-sm">
@ -156,11 +141,12 @@ const departments = ref();
</QItem>
<QItem class="q-mb-sm">
<QItemSection v-if="authors">
<QItemSection>
<VnSelect
data-key="author"
:input-debounce="0"
:label="t('Author')"
:options="authors"
url="Workers/activeWithInheritedRole"
dense
emit-value
hide-selected
@ -174,9 +160,6 @@ const departments = ref();
@update:model-value="searchFn()"
/>
</QItemSection>
<QItemSection v-else>
<QSkeleton class="full-width" type="QInput" />
</QItemSection>
</QItem>
<QItem class="q-mb-sm">

View File

@ -11,7 +11,7 @@ describe('EntryMy when is supplier', () => {
it('should open buyLabel when is supplier', () => {
cy.get(
'[to="/null/2"] > .q-card > .column > .q-btn > .q-btn__content > .q-icon'
'.q-table__container>.q-table__grid-content >span:nth-child(4 )> .q-card > .column > .q-btn > .q-btn__content > .q-icon'
).click();
cy.get('.q-card__actions > .q-btn').click();
cy.window().its('open').should('be.called');

View File

@ -0,0 +1,137 @@
import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest';
import { createWrapper, axios } from 'app/test/vitest/helper';
import VnSelect from 'src/components/common/VnSelect.vue';
describe('VnSelect use options as arguments', () => {
let vm;
const options = [
{ id: 1, name: 'Option 1' },
{ id: 2, name: 'Option 2' },
{ id: 3, name: 'Option 3' },
];
beforeAll(() => {
vm = createWrapper(VnSelect, {
propsData: {
options,
optionLabel: 'name',
optionValue: 'id',
},
global: {
stubs: ['FetchData'],
mocks: {},
},
}).vm;
});
afterEach(() => {
vi.clearAllMocks();
});
it('should mounted correctly', () => {
expect(vm).toBeDefined();
});
it('should pass options and not CALL URL', async () => {
expect(vm.myOptions.length).toEqual(options.length);
expect(vm.useURL).toBeFalsy();
vm.selectValue = 'Option 2';
const optionsFiltered = vm.filter('Option 2', options);
// vm.filterHandler('Option 2', vi.fn());
vm.filterHandler('Option 2', () => {
vm.myOptions = optionsFiltered;
});
expect(vm.myOptions.length).toEqual(1);
});
});
describe('VnSelect CALL FETCH URL', () => {
let vm;
beforeAll(async () => {
vi.spyOn(axios, 'get').mockResolvedValue({
data: [
{ id: 1, name: 'Tony Stark' },
{ id: 2, name: 'Jessica Jones' },
{ id: 3, name: 'Bruce Wayne' },
],
});
vm = createWrapper(VnSelect, {
propsData: {
optionLabel: 'name',
optionValue: 'id',
url: 'Suppliers',
modelValue: '1',
sortBy: 'name DESC',
limit: '320',
fields: ['name'],
},
global: {
stubs: ['FetchData'],
mocks: { fetch: vi.fn() },
},
}).vm;
});
afterEach(() => {
vi.clearAllMocks();
});
it('should CALL URL', async () => {
vm.selectValue = '';
expect(vm.options.length).toEqual(0);
expect(vm.useURL).toBeTruthy();
expect(vm.myOptions.length).toEqual(3);
const canceller = new AbortController();
expect(axios.get).toHaveBeenCalledWith('Suppliers', {
params: {
filter: '{"limit":"320","where":{"id":{"like":"%1%"}},"include":null,"fields":["name"],"order":"name DESC","skip":0}',
},
signal: canceller.signal,
});
});
});
describe('VnSelect NOT CALL FETCH URL', () => {
let vm;
beforeAll(async () => {
vi.spyOn(axios, 'get').mockResolvedValue({
data: [
{ id: 1, name: 'Tony Stark' },
{ id: 2, name: 'Jessica Jones' },
{ id: 3, name: 'Bruce Wayne' },
],
});
vm = createWrapper(VnSelect, {
propsData: {
optionLabel: 'name',
optionValue: 'id',
url: 'Suppliers',
sortBy: 'name DESC',
limit: '320',
fields: ['name'],
},
global: {
stubs: ['FetchData'],
mocks: { fetch: vi.fn() },
},
}).vm;
});
afterEach(() => {
vi.clearAllMocks();
});
it('should NOT CALL URL because no modelValue exist', async () => {
vm.selectValue = '';
expect(vm.options.length).toEqual(0);
expect(vm.useURL).toBeTruthy();
expect(vm.myOptions.length).toEqual(0);
const canceller = new AbortController();
expect(axios.get).not.toHaveBeenCalledWith('Suppliers', {
params: {
filter: '{"limit":"320","where":{},"order":"name DESC","include":null,"fields":["name"],"skip":0}',
},
signal: canceller.signal,
});
});
});