#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']);
jsegarra marked this conversation as resolved Outdated
Outdated
Review

Quitar

Quitar
@ -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: () => {},
},
Outdated
Review

Por como estaba creo recordar que myOptions era la variable para las opciones filtradas y myOptionsOriginal para las originales. myOptionsFiltered para que seria? Puede ser myOptions ?

Por como estaba creo recordar que `myOptions` era la variable para las opciones filtradas y `myOptionsOriginal ` para las originales. `myOptionsFiltered ` para que seria? Puede ser `myOptions ` ?

Me he dado cuenta que la necesidad de esa variable no cubre otro caso de uso asi que la elimino

Me he dado cuenta que la necesidad de esa variable no cubre otro caso de uso asi que la elimino
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();
jsegarra marked this conversation as resolved Outdated
Outdated
Review

De primeras diria que esta linea no hace falta, dado que la primera ja lo hace

De primeras diria que esta linea no hace falta, dado que la primera ja lo hace

Pero quiero evitar que haga fetch
Un caso de uso sería: se lo declaro en el componente o una variable

Pero quiero evitar que haga fetch Un caso de uso sería: se lo declaro en el componente o una variable
Outdated
Review

updateRouter: false sino podrá cosas en la url

`updateRouter: false` sino podrá cosas en la url
const lastVal = ref();
Outdated
Review

Esta parte solo debe hacerla si hay url. Y por como esta ahora ya habria hecho la peticion al entrar en if (useURL.value)

Esta parte solo debe hacerla si hay url. Y por como esta ahora ya habria hecho la peticion al entrar en `if (useURL.value)`

Lo cambio para dejarlo asi
if (!$props.options) fetchFilter($props.modelValue);

Lo cambio para dejarlo asi if (!$props.options) fetchFilter($props.modelValue);
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() {
Outdated
Review

Que esten estas dos funciones a la vez lo veo raro (fetchFilter, initSelect)
Pq si no hay options y hay URL hara la peticion 2 veces. Yo diria que la funcion de initSelect no hace falta.

De hecho yo creo que solo deberia haber un fetch y cuando se quiera hacer un fetch se llame ahi (fetchFilter)

Que esten estas dos funciones a la vez lo veo raro (fetchFilter, initSelect) Pq si no hay options y hay URL hara la peticion 2 veces. Yo diria que la funcion de initSelect no hace falta. De hecho yo creo que solo deberia haber un fetch y cuando se quiera hacer un fetch se llame ahi (fetchFilter)

Creía que habían mas ocurrencias de initselect, lo he eliminado

Creía que habían mas ocurrencias de initselect, lo he eliminado
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));
jsegarra marked this conversation as resolved Outdated
Outdated
Review

Por como esta useArrayData, fetch no acepta los datos por paramtros. Solo { append = false, updateRouter = true }

Por como esta useArrayData, fetch no acepta los datos por paramtros. Solo `{ append = false, updateRouter = true }`
myOptionsOriginal.value = JSON.parse(JSON.stringify(data));
if (append) myOptionsOriginal.value = JSON.parse(JSON.stringify(data));
}
function filter(val, options) {
Outdated
Review

Si de por si solo entra en fetchFilter si hay url, no hace falta separar la construccion del where en una funcion apart, que solo se usa en un sitio

Si de por si solo entra en fetchFilter si hay url, no hace falta separar la construccion del where en una funcion apart, que solo se usa en un sitio

Vale, lo debí separar para otra situación que acabé borrando y esto se quedó así
Lo limipio

Vale, lo debí separar para otra situación que acabé borrando y esto se quedó así Lo limipio
@ -165,7 +170,7 @@ function filter(val, options) {
}
async function fetchFilter(val) {
jsegarra marked this conversation as resolved Outdated
Outdated
Review

Quitar

Quitar
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);
Outdated
Review

Usando arrayData + lo que hablamos ayer creo que ya no hace falta el FetchData

Usando arrayData + lo que hablamos ayer creo que ya no hace falta el FetchData
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,
});
});
});