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

View File

@ -235,8 +235,12 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
async function loadMore() { async function loadMore() {
if (!store.hasMoreData) return; 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; store.page += 1;
await fetch({ append: true }); await fetch({ append: true });

View File

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

View File

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

View File

@ -11,7 +11,7 @@ describe('EntryMy when is supplier', () => {
it('should open buyLabel when is supplier', () => { it('should open buyLabel when is supplier', () => {
cy.get( 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(); ).click();
cy.get('.q-card__actions > .q-btn').click(); cy.get('.q-card__actions > .q-btn').click();
cy.window().its('open').should('be.called'); 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,
});
});
});