0
0
Fork 0

feat(url): sepate filters

This commit is contained in:
Alex Moreno 2024-06-10 09:19:42 +02:00
parent cb4a5f8d1b
commit bbf847e1a0
11 changed files with 165 additions and 206 deletions

View File

@ -22,9 +22,13 @@ const $props = defineProps({
type: String,
required: true,
},
searchUrl: {
type: String,
default: 'params',
},
});
const model = defineModel();
const arrayData = useArrayData($props.dataKey);
const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl });
const columnFilter = computed(() => $props.column?.columnFilter);
const updateEvent = { 'update:modelValue': addFilter };
@ -99,6 +103,7 @@ const components = {
};
async function addFilter(value) {
value ||= undefined;
if (value && typeof value === 'object') value = model.value;
value = value === '' ? undefined : value;
let field = columnFilter.value?.name ?? $props.column.name;

View File

@ -1,7 +1,7 @@
<script setup>
import { ref, onMounted, computed, watch } from 'vue';
import { ref, onMounted, computed, watch, useAttrs } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
@ -47,15 +47,22 @@ const $props = defineProps({
type: String,
default: 'flex-one',
},
searchUrl: {
type: String,
default: 'table',
},
});
const attrs = useAttrs();
const { t } = useI18n();
const stateStore = useStateStore();
const route = useRoute();
const router = useRouter();
const quasar = useQuasar();
const mode = ref('card');
const selected = ref([]);
const params = ref({});
const routeQuery = JSON.parse(route?.query[$props.searchUrl] ?? '{}');
const params = ref({ ...routeQuery, ...routeQuery.filter?.where });
const VnPaginateRef = ref({});
const showForm = ref(false);
const splittedColumns = ref({ columns: [] });
@ -76,6 +83,7 @@ const tableModes = [
onMounted(() => {
mode.value = quasar.platform.is.mobile ? 'card' : $props.defaultMode;
stateStore.rightDrawer = true;
setUserParams(route.query[$props.searchUrl]);
});
watch(
@ -84,6 +92,27 @@ watch(
{ immediate: true }
);
// try without
watch(
() => route.params.id,
() => reload(attrs)
);
watch(
() => route.query[$props.searchUrl],
(val) => setUserParams(val)
);
function setUserParams(watchedParams) {
if (!watchedParams) return;
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
const where = JSON.parse(watchedParams?.filter)?.where;
watchedParams = { ...watchedParams, ...where };
delete watchedParams.filter;
params.value = { ...params.value, ...watchedParams };
}
function splitColumns(columns) {
splittedColumns.value = {
columns: [],
@ -130,12 +159,12 @@ function stopEventPropagation(event) {
event.stopPropagation();
}
function reload() {
VnPaginateRef.value.fetch();
function reload(params) {
VnPaginateRef.value.fetch(params);
}
function columnName(col) {
const column = Object.assign({}, col, col.columnFilter);
const column = { ...col, ...col.columnFilter };
let name = column.name;
if (column.alias) name = column.alias + '.' + name;
return name;
@ -160,6 +189,7 @@ defineExpose({
:search-button="true"
v-model="params"
:disable-submit-event="true"
:search-url="searchUrl"
>
<template #body>
<VnTableFilter
@ -183,6 +213,7 @@ defineExpose({
class="q-px-md"
:limit="20"
ref="VnPaginateRef"
:search-url="searchUrl"
:disable-infinite-scroll="mode == 'table'"
>
<template #body="{ rows }">
@ -237,6 +268,7 @@ defineExpose({
:show-title="true"
:data-key="$attrs['data-key']"
v-model="params[columnName(col)]"
:search-url="searchUrl"
/>
</QTh>
</template>

View File

@ -1,6 +1,6 @@
<script setup>
import { onBeforeMount, computed, watchEffect } from 'vue';
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
import { useRoute } from 'vue-router';
import { useArrayData } from 'src/composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
import useCardSize from 'src/composables/useCardSize';
@ -39,15 +39,6 @@ onBeforeMount(async () => {
await arrayData.fetch({ append: false });
});
if (props.baseUrl) {
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
arrayData.store.url = `${props.baseUrl}/${route.params.id}`;
await arrayData.fetch({ append: false });
}
});
}
watchEffect(() => {
if (Array.isArray(arrayData.store.data))
arrayData.store.data = arrayData.store.data[0];

View File

@ -180,6 +180,7 @@ watch(modelValue, (newValue) => {
>
<template v-if="isClearable" #append>
<QIcon
v-show="value"
name="close"
@click.stop="value = null"
class="cursor-pointer"

View File

@ -4,12 +4,11 @@ import { useI18n } from 'vue-i18n';
import { useArrayData } from 'composables/useArrayData';
import { useRoute } from 'vue-router';
import toDate from 'filters/toDate';
import useRedirect from 'src/composables/useRedirect';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
const { t } = useI18n();
const params = defineModel();
const props = defineProps({
const params = defineModel({ default: {}, required: true, type: Object });
const $props = defineProps({
dataKey: {
type: String,
required: true,
@ -46,82 +45,72 @@ const props = defineProps({
type: Boolean,
default: false,
},
searchUrl: {
type: String,
default: 'params',
},
});
const emit = defineEmits(['refresh', 'clear', 'search', 'init', 'remove']);
const arrayData = useArrayData(props.dataKey, {
exprBuilder: props.exprBuilder,
const arrayData = useArrayData($props.dataKey, {
exprBuilder: $props.exprBuilder,
searchUrl: $props.searchUrl,
navigate: {},
});
const route = useRoute();
const store = arrayData.store;
const userParams = ref({});
const { navigate } = useRedirect();
onMounted(() => {
if (params.value) userParams.value = JSON.parse(JSON.stringify(params.value));
if (Object.keys(store.userParams).length > 0) {
userParams.value = JSON.parse(JSON.stringify(store.userParams));
params.value = {
...params.value,
...userParams.value,
...userParams.value?.filter?.where,
};
}
emit('init', { params: userParams.value });
emit('init', { params: params.value });
});
function setUserParams(params) {
if (!params) {
userParams.value = {};
} else {
userParams.value = typeof params == 'string' ? JSON.parse(params) : params;
}
function setUserParams(watchedParams) {
if (!watchedParams) return;
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
watchedParams = { ...watchedParams, ...watchedParams.filter?.where };
delete watchedParams.filter;
params.value = { ...params.value, ...watchedParams };
}
watch(
() => route.query.params,
(val) => {
setUserParams(val);
}
() => route.query[$props.searchUrl],
(val) => setUserParams(val)
);
watch(
() => arrayData.store.userParams,
(val) => {
setUserParams(val);
}
(val) => setUserParams(val)
);
const isLoading = ref(false);
async function search(evt) {
if (evt && props.disableSubmitEvent) return;
if (evt && $props.disableSubmitEvent) return;
store.filter.where = {};
isLoading.value = true;
const filter = { ...userParams.value, ...params.value };
const filter = { ...params.value };
store.userParamsChanged = true;
store.filter.skip = 0;
store.skip = 0;
const { params: newParams } = await arrayData.addFilter({ params: filter });
userParams.value = newParams;
const { params: newParams } = await arrayData.addFilter({ params: params.value });
params.value = newParams;
if (!props.showAll && !Object.values(filter).length) store.data = [];
if (!$props.showAll && !Object.values(filter).length) store.data = [];
isLoading.value = false;
emit('search');
navigate(store.data, {});
}
async function reload() {
isLoading.value = true;
const params = Object.values(userParams.value).filter((param) => param);
const params = Object.values(params.value).filter((param) => param);
await arrayData.fetch({ append: false });
if (!props.showAll && !params.length) store.data = [];
if (!$props.showAll && !params.length) store.data = [];
isLoading.value = false;
emit('refresh');
navigate(store.data, {});
}
async function clearFilters() {
@ -130,19 +119,19 @@ async function clearFilters() {
store.filter.skip = 0;
store.skip = 0;
// Filtrar los params no removibles
const removableFilters = Object.keys(userParams.value).filter((param) =>
props.unremovableParams.includes(param)
const removableFilters = Object.keys(params.value).filter((param) =>
$props.unremovableParams.includes(param)
);
const newParams = {};
// Conservar solo los params que no son removibles
for (const key of removableFilters) {
newParams[key] = userParams.value[key];
newParams[key] = params.value[key];
}
params.value = {};
userParams.value = { ...newParams }; // Actualizar los params con los removibles
await arrayData.applyFilter({ params: userParams.value });
params.value = { ...newParams }; // Actualizar los params con los removibles
await arrayData.applyFilter({ params: params.value });
if (!props.showAll) {
if (!$props.showAll) {
store.data = [];
}
@ -152,34 +141,26 @@ async function clearFilters() {
const tagsList = computed(() => {
const tagList = [];
const params = {
...userParams.value,
};
const where = params?.filter?.where;
if (where) {
Object.assign(params, where);
}
delete params.filter;
for (const key of Object.keys(params)) {
const value = params[key];
if (!value || (props.hiddenTags || []).includes(key)) continue;
tagList.push({ key, value });
for (const key of Object.keys(params.value)) {
const value = params.value[key];
if (!value || ($props.hiddenTags || []).includes(key)) continue;
tagList.push({ label: key, value });
}
return tagList;
});
const tags = computed(() =>
tagsList.value.filter((tag) => !(props.customTags || []).includes(tag.key))
);
const tags = computed(() => {
return tagsList.value.filter((tag) => !($props.customTags || []).includes(tag.key));
});
const customTags = computed(() =>
tagsList.value.filter((tag) => (props.customTags || []).includes(tag.key))
tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.key))
);
async function remove(key) {
delete userParams.value[key];
delete userParams.value.filter.where[key];
delete params.value[key];
delete params.value.filter?.where[key];
params.value[key] = undefined;
await arrayData.applyFilter({ params: userParams.value });
await arrayData.applyFilter({ params: params.value });
emit('remove', key);
}
@ -242,15 +223,16 @@ function formatValue(value) {
{{ t(`No filters applied`) }}
</div>
<div>
{{ tags }}
<VnFilterPanelChip
v-for="chip of tags"
:key="chip.key"
:removable="!unremovableParams.includes(chip.key)"
@remove="remove(chip.key)"
:key="chip.label"
:removable="!unremovableParams.includes(chip.label)"
@remove="remove(chip.label)"
>
<slot name="tags" :tag="chip" :format-fn="formatValue">
<div class="q-gutter-x-xs">
<strong>{{ chip.key }}:</strong>
<strong>{{ chip.label }}:</strong>
<span>"{{ chip.value }}"</span>
</div>
</slot>
@ -258,7 +240,7 @@ function formatValue(value) {
<slot
v-if="$slots.customTags"
name="customTags"
:params="userParams"
:params="params"
:tags="customTags"
:format-fn="formatValue"
:search-fn="search"
@ -268,9 +250,9 @@ function formatValue(value) {
<QSeparator />
</QList>
<QList dense class="list q-gutter-y-sm q-mt-sm">
<slot name="body" :params="userParams" :search-fn="search"></slot>
<slot name="body" :params="params" :search-fn="search"></slot>
</QList>
<template v-if="props.searchButton">
<template v-if="$props.searchButton">
<QItem>
<QItemSection class="q-py-sm">
<QBtn
@ -282,7 +264,6 @@ function formatValue(value) {
rounded
:type="disableSubmitEvent ? 'button' : 'submit'"
unelevated
@click="search()"
/>
</QItemSection>
</QItem>
@ -295,7 +276,6 @@ function formatValue(value) {
color="primary"
/>
</template>
<style scoped lang="scss">
.list {
width: 256px;

View File

@ -58,6 +58,10 @@ const props = defineProps({
type: Function,
default: null,
},
searchUrl: {
type: String,
default: null,
},
disableInfiniteScroll: {
type: Boolean,
default: false,
@ -81,6 +85,7 @@ const arrayData = useArrayData(props.dataKey, {
userParams: props.userParams,
exprBuilder: props.exprBuilder,
keepOpts: props.keepOpts,
searchUrl: props.searchUrl,
});
const store = arrayData.store;
@ -99,7 +104,8 @@ const addFilter = async (filter, params) => {
await arrayData.addFilter({ filter, params });
};
async function fetch() {
async function fetch(params) {
useArrayData(props.dataKey, params);
store.filter.skip = 0;
store.skip = 0;
await arrayData.fetch({ append: false });

View File

@ -3,7 +3,6 @@ import { onMounted, ref } from 'vue';
import { useQuasar } from 'quasar';
import { useArrayData } from 'composables/useArrayData';
import VnInput from 'src/components/common/VnInput.vue';
import useRedirect from 'src/composables/useRedirect';
import { useI18n } from 'vue-i18n';
const quasar = useQuasar();
@ -16,17 +15,14 @@ const props = defineProps({
},
label: {
type: String,
required: false,
default: 'Search',
},
info: {
type: String,
required: false,
default: '',
},
redirect: {
type: Boolean,
required: false,
default: true,
},
url: {
@ -67,10 +63,20 @@ const props = defineProps({
},
});
const arrayData = useArrayData(props.dataKey, { ...props });
const { store } = arrayData;
const searchText = ref('');
const { navigate } = useRedirect();
let arrayDataProps = { ...props };
if (props.redirect)
arrayDataProps = {
...props,
...{
navigate: {
customRouteRedirectName: props.customRouteRedirectName,
searchText: searchText.value,
},
},
};
let arrayData = useArrayData(props.dataKey, arrayDataProps);
let store = arrayData.store;
onMounted(() => {
const params = store.userParams;
@ -90,13 +96,6 @@ async function search() {
search: searchText.value,
},
});
if (!props.redirect) return;
navigate(store.data, {
customRouteRedirectName: props.customRouteRedirectName,
searchText: searchText.value,
});
}
</script>

View File

@ -23,8 +23,13 @@ export function useArrayData(key, userOptions) {
store.skip = 0;
const query = route.query;
if (query.params) {
store.userParams = JSON.parse(query.params);
const searchUrl = store.searchUrl;
if (query[searchUrl]) {
const params = JSON.parse(query[searchUrl]);
const filter = params?.filter;
delete params.filter;
store.userParams = { ...params, ...store.userParams };
store.userFilter = { ...JSON.parse(filter), ...store.userFilter };
}
});
@ -41,13 +46,15 @@ export function useArrayData(key, userOptions) {
'userParams',
'userFilter',
'exprBuilder',
'searchUrl',
'navigate',
];
if (typeof userOptions === 'object') {
for (const option in userOptions) {
const isEmpty = userOptions[option] == null || userOptions[option] === '';
if (isEmpty || !allowedOptions.includes(option)) continue;
if (Object.prototype.hasOwnProperty.call(store, option)) {
if (Object.hasOwn(store, option)) {
const defaultOpts = userOptions[option];
store[option] = userOptions.keepOpts?.includes(option)
? Object.assign(defaultOpts, store[option])
@ -88,8 +95,8 @@ export function useArrayData(key, userOptions) {
Object.assign(params, userParams);
store.isLoading = true;
store.currentFilter = params;
store.isLoading = true;
const response = await axios.get(store.url, {
signal: canceller.signal,
params,
@ -119,6 +126,10 @@ export function useArrayData(key, userOptions) {
}
}
function deleteOption(option) {
delete store[option];
}
function cancelRequest() {
if (canceller) {
canceller.abort();
@ -129,7 +140,7 @@ export function useArrayData(key, userOptions) {
async function applyFilter({ filter, params }) {
if (filter) store.userFilter = filter;
store.filter = {};
if (params) store.userParams = Object.assign({}, params);
if (params) store.userParams = { ...params };
const response = await fetch({ append: false });
return response;
@ -138,7 +149,7 @@ export function useArrayData(key, userOptions) {
async function addFilter({ filter, params }) {
if (filter) store.userFilter = Object.assign(store.userFilter, filter);
let userParams = Object.assign({}, store.userParams, params);
let userParams = { ...store.userParams, ...params };
userParams = sanitizerParams(userParams, store?.exprBuilder);
store.userParams = userParams;
@ -163,9 +174,7 @@ export function useArrayData(key, userOptions) {
delete store.userParams[param];
delete params[param];
if (store.filter?.where) {
const key = Object.keys(
exprBuilder && exprBuilder(param) ? exprBuilder(param) : param
);
const key = Object.keys(exprBuilder ? exprBuilder(param) : param);
if (key[0]) delete store.filter.where[key[0]];
if (Object.keys(store.filter.where).length === 0) {
delete store.filter.where;
@ -190,22 +199,32 @@ export function useArrayData(key, userOptions) {
}
function updateStateParams() {
const query = {};
if (store.order) query.order = store.order;
if (store.limit) query.limit = store.limit;
if (store.skip) query.skip = store.skip;
if (store.userParams && Object.keys(store.userParams).length !== 0)
query.params = store.userParams;
if (store.userFilter && Object.keys(store.userFilter).length !== 0) {
if (!query.params) query.params = {};
query.params.filter = store.userFilter;
}
if (query.params) query.params = JSON.stringify(query.params);
const newUrl = { path: route.path, query: { ...(route.query ?? {}) } };
newUrl.query[store.searchUrl] = JSON.stringify(store.currentFilter);
router.replace({
path: route.path,
query,
if (store.navigate) {
const { customRouteRedirectName, searchText } = store.navigate;
if (customRouteRedirectName)
return router.push({
name: customRouteRedirectName,
params: { id: searchText },
});
const { matched: matches } = router.currentRoute.value;
const { path } = matches.at(-1);
const to =
store?.data?.length === 1
? path.replace(/\/(list|:id)|-list/, `/${store.data[0].id}`)
: path.replace(/:id.*/, '');
if (route.path != to) {
store.userParams = {};
store.userFilter = {};
return router.push({ path: to });
}
}
router.replace(newUrl);
}
const totalRows = computed(() => (store.data && store.data.length) || 0);
@ -223,5 +242,6 @@ export function useArrayData(key, userOptions) {
totalRows,
updateStateParams,
isLoading,
deleteOption,
};
}

View File

@ -1,25 +0,0 @@
import { useRouter } from 'vue-router';
export default function useRedirect() {
const router = useRouter();
const navigate = (data, { customRouteRedirectName, searchText }) => {
if (customRouteRedirectName)
return router.push({
name: customRouteRedirectName,
params: { id: searchText },
});
const { matched: matches } = router.currentRoute.value;
const { path } = matches.at(-1);
const to =
data.length === 1
? path.replace(/\/(list|:id)|-list/, `/${data[0].id}`)
: path.replace(/:id.*/, '');
router.push({ path: to });
};
return { navigate };
}

View File

@ -21,6 +21,8 @@ export const useArrayDataStore = defineStore('arrayDataStore', () => {
isLoading: false,
userParamsChanged: false,
exprBuilder: null,
searchUrl: 'params',
navigate: null,
};
}

View File

@ -1,52 +0,0 @@
import { vi, describe, expect, it, beforeEach, beforeAll } from 'vitest';
import useRedirect from 'src/composables/useRedirect';
import { useRouter } from 'vue-router';
vi.mock('vue-router');
describe('useRedirect', () => {
useRouter.mockReturnValue({
push: vi.fn(),
currentRoute: {
value: {
matched: [
{ path: '/' },
{ path: '/customer' },
{ path: '/customer/:id' },
{ path: '/customer/:id/basic-data' },
],
},
},
});
const data = [];
let navigate;
let spy;
beforeAll(() => {
const { navigate: navigateFn } = useRedirect();
navigate = navigateFn;
spy = useRouter().push;
});
beforeEach(() => {
data.length = 0;
spy.mockReset();
});
it('should redirect to list page if there are several results', async () => {
data.push({ id: 1, name: 'employee' }, { id: 2, name: 'boss' });
navigate(data, {});
expect(spy).toHaveBeenCalledWith({ path: '/customer/' });
});
it('should redirect to list page if there is no results', async () => {
navigate(data, {});
expect(spy).toHaveBeenCalledWith({ path: '/customer/' });
});
it('should redirect to basic-data page if there is only one result', async () => {
data.push({ id: 1, name: 'employee' });
navigate(data, {});
expect(spy).toHaveBeenCalledWith({ path: '/customer/1/basic-data' });
});
});