Merge branch 'dev' into 6919-reactiveDescriptor
gitea/salix-front/pipeline/pr-dev This commit looks good Details

This commit is contained in:
Jorge Penadés 2024-11-12 12:36:48 +00:00
commit 90f04cc010
6 changed files with 360 additions and 196 deletions

View File

@ -119,7 +119,7 @@ watch(options, (newValue) => {
}); });
watch(modelValue, async (newValue) => { watch(modelValue, async (newValue) => {
if (!myOptions.value.some((option) => option[optionValue.value] == newValue)) if (!myOptions?.value?.some((option) => option[optionValue.value] == newValue))
await fetchFilter(newValue); await fetchFilter(newValue);
if ($props.noOne) myOptions.value.unshift(noOneOpt.value); if ($props.noOne) myOptions.value.unshift(noOneOpt.value);

View File

@ -57,7 +57,6 @@ const $props = defineProps({
}, },
}); });
defineExpose({ search, sanitizer });
const emit = defineEmits([ const emit = defineEmits([
'update:modelValue', 'update:modelValue',
'refresh', 'refresh',
@ -170,9 +169,29 @@ const tagsList = computed(() => {
return tagList; return tagList;
}); });
const formatTags = (tags) => {
const formattedTags = [];
tags.forEach((tag) => {
if (tag.label === 'and') {
tag.value.forEach((item) => {
for (const key in item) {
formattedTags.push({ label: key, value: item[key] });
}
});
} else {
formattedTags.push(tag);
}
});
return formattedTags;
};
const tags = computed(() => { const tags = computed(() => {
return tagsList.value.filter((tag) => !($props.customTags || []).includes(tag.label)); const filteredTags = tagsList.value.filter(
(tag) => !($props.customTags || []).includes(tag.label)
);
return formatTags(filteredTags);
}); });
const customTags = computed(() => const customTags = computed(() =>
tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.label)) tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.label))
); );
@ -193,13 +212,20 @@ function formatValue(value) {
function sanitizer(params) { function sanitizer(params) {
for (const [key, value] of Object.entries(params)) { for (const [key, value] of Object.entries(params)) {
if (value && typeof value === 'object') { if (key === 'and' && Array.isArray(value)) {
value.forEach((item) => {
Object.assign(params, item);
});
delete params[key];
} else if (value && typeof value === 'object') {
const param = Object.values(value)[0]; const param = Object.values(value)[0];
if (typeof param == 'string') params[key] = param.replaceAll('%', ''); if (typeof param == 'string') params[key] = param.replaceAll('%', '');
} }
} }
return params; return params;
} }
defineExpose({ search, sanitizer, userParams });
</script> </script>
<template> <template>

View File

@ -10,11 +10,19 @@ function parseJSON(str, fallback) {
export default function (route, param) { export default function (route, param) {
// catch route query params // catch route query params
const params = parseJSON(route?.query?.params, {}); const params = parseJSON(route?.query?.params, {});
// extract and parse filter from params // extract and parse filter from params
const { filter: filterStr = '{}' } = params; const { filter: filterStr = '{}' } = params;
const where = parseJSON(filterStr, {})?.where; const where = parseJSON(filterStr, {})?.where;
if (where && where[param] !== undefined) {
if (where && !param) {
return where;
} else if (where && where.and) {
const foundParam = where.and.find((p) => p[param]);
if (foundParam) {
return foundParam[param];
}
} else if (where && where[param]) {
return where[param]; return where[param];
} }
return null; return null;

View File

@ -0,0 +1,163 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
const props = defineProps({
tags: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['applyTags']);
const { t } = useI18n();
const tagValues = ref([{}]);
const tagOptions = ref([]);
const selectedTag = ref(null);
const applyTags = () => {
if (tagValues.value.some((tag) => !tag.value)) return;
const tagInfo = {
values: [...tagValues.value],
tagFk: selectedTag?.value?.id,
tagSelection: {
name: selectedTag?.value?.name,
},
};
emit('applyTags', tagInfo);
};
const removeTagGroupParam = (valIndex = null) => {
if (!valIndex) {
tagValues.value = [{}];
} else {
(tagValues.value || []).splice(valIndex, 1);
}
};
const getSelectedTagValues = async (tag) => {
try {
if (!tag?.id) return;
const filter = {
fields: ['value'],
order: 'value ASC',
limit: 30,
};
const url = `Tags/${tag?.id}/filterValue`;
const params = { filter: JSON.stringify(filter) };
const { data } = await axios.get(url, {
params,
});
tagOptions.value = data;
} catch (err) {
console.error('Error getting selected tag values');
}
};
</script>
<template>
<QForm @submit="applyTags(tagValues)" class="all-pointer-events">
<QCard class="q-pa-sm column q-pa-lg">
<QBtn
round
color="primary"
style="position: absolute; z-index: 1; right: 0; top: 0"
icon="search"
type="submit"
>
</QBtn>
<VnSelect
:label="t('params.tag')"
v-model="selectedTag"
:options="props.tags"
option-value="id"
option-label="name"
dense
outlined
class="q-mb-md"
rounded
:emit-value="false"
use-input
@update:model-value="($event) => getSelectedTagValues($event)"
/>
<QBtn
icon="add_circle"
shortcut="+"
flat
class="filter-icon q-mb-md"
size="md"
dense
:disabled="!selectedTag || !tagValues[0].value"
@click="tagValues.unshift({})"
/>
<div
v-for="(value, index) in tagValues"
:key="value"
class="filter-value column align-left"
>
<div class="col row q-mb-md">
<VnSelect
v-if="!selectedTag?.isFree && tagOptions"
:label="t('components.itemsFilterPanel.value')"
v-model="value.value"
:options="tagOptions || []"
option-value="value"
option-label="value"
dense
outlined
rounded
emit-value
use-input
:disable="!value || !selectedTag"
:is-clearable="false"
class="col"
/>
<VnInput
v-else
v-model="value.value"
:label="t('components.itemsFilterPanel.value')"
:disable="!value"
is-outlined
:is-clearable="false"
class="col"
/>
<QBtn
icon="delete"
size="md"
outlined
dense
rounded
flat
class="filter-icon col-2"
:disabled="!value.value"
@click="removeTagGroupParam(index)"
/>
</div>
</div>
</QCard>
</QForm>
</template>
<style scoped lang="scss">
.filter-icon {
font-size: 24px;
color: $primary;
padding: 0 4px;
cursor: pointer;
}
</style>
<i18n>
en:
params:
tag: Tag
es:
params:
tag: Etiqueta
</i18n>

View File

@ -1,30 +1,57 @@
<script setup> <script setup>
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { onMounted, onUnmounted, ref } from 'vue'; import { onBeforeMount, onMounted, onUnmounted, ref, computed, watch } from 'vue';
import axios from 'axios'; import axios from 'axios';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnPaginate from 'components/ui/VnPaginate.vue'; import VnPaginate from 'components/ui/VnPaginate.vue';
import CatalogItem from 'components/ui/CatalogItem.vue'; import CatalogItem from 'components/ui/CatalogItem.vue';
import OrderCatalogFilter from 'pages/Order/Card/OrderCatalogFilter.vue'; import OrderCatalogFilter from 'pages/Order/Card/OrderCatalogFilter.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import getParamWhere from 'src/filters/getParamWhere';
import { useArrayData } from 'composables/useArrayData';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const stateStore = useStateStore(); const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const arrayData = useArrayData('OrderCatalogList');
const store = arrayData.store;
const showFilter = ref(null);
const tags = ref([]); const tags = ref([]);
let catalogParams = {
orderFk: route.params.id,
orderBy: JSON.stringify({ field: 'relevancy DESC, name', way: 'ASC', isTag: false }),
};
onBeforeMount(() => {
const whereParams = getParamWhere(route);
if (whereParams) {
const formattedWhereParams = {};
if (whereParams.and) {
whereParams.and.forEach((item) => {
Object.assign(formattedWhereParams, item);
});
} else {
Object.assign(formattedWhereParams, whereParams);
}
catalogParams = {
...catalogParams,
...formattedWhereParams,
};
} else {
showFilter.value = true;
}
});
onMounted(() => { onMounted(() => {
stateStore.rightDrawer = true; stateStore.rightDrawer = true;
checkOrderConfirmation(); checkOrderConfirmation();
}); });
onUnmounted(() => (stateStore.rightDrawer = false));
const catalogParams = { onUnmounted(() => (stateStore.rightDrawer = false));
orderFk: route.params.id,
orderBy: JSON.stringify({ field: 'relevancy DESC, name', way: 'ASC', isTag: false }),
};
async function checkOrderConfirmation() { async function checkOrderConfirmation() {
const response = await axios.get(`Orders/${route.params.id}`); const response = await axios.get(`Orders/${route.params.id}`);
@ -34,6 +61,7 @@ async function checkOrderConfirmation() {
} }
function extractTags(items) { function extractTags(items) {
if (!items || !items.length) return;
const resultTags = []; const resultTags = [];
(items || []).forEach((item) => { (items || []).forEach((item) => {
(item.tags || []).forEach((tag) => { (item.tags || []).forEach((tag) => {
@ -61,6 +89,15 @@ function extractValueTags(items) {
); );
tagValue.value = resultValueTags; tagValue.value = resultValueTags;
} }
const autoLoad = computed(() => !!catalogParams.categoryFk);
watch(
() => store.data,
(val) => {
extractTags(val);
},
{ immediate: true }
);
</script> </script>
<template> <template>
@ -74,11 +111,12 @@ function extractValueTags(items) {
:info="t('You can search items by name or id')" :info="t('You can search items by name or id')"
/> />
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> <QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8"> <QScrollArea v-if="showFilter" class="fit text-grey-8">
<OrderCatalogFilter <OrderCatalogFilter
data-key="OrderCatalogList" data-key="OrderCatalogList"
:tag-value="tagValue" :tag-value="tagValue"
:tags="tags" :tags="tags"
:initial-catalog-params="catalogParams"
/> />
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
@ -89,8 +127,9 @@ function extractValueTags(items) {
url="Orders/CatalogFilter" url="Orders/CatalogFilter"
:limit="50" :limit="50"
:user-params="catalogParams" :user-params="catalogParams"
@on-fetch="extractTags" @on-fetch="showFilter = true"
:update-router="false" :update-router="false"
:auto-load="autoLoad"
> >
<template #body="{ rows }"> <template #body="{ rows }">
<div class="catalog-list"> <div class="catalog-list">

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed, ref } from 'vue'; import { computed, ref, onMounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
@ -9,10 +9,9 @@ import VnSelect from 'components/common/VnSelect.vue';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue'; import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import getParamWhere from 'src/filters/getParamWhere'; import getParamWhere from 'src/filters/getParamWhere';
import CatalogFilterValueDialog from 'src/pages/Order/Card/CatalogFilterValueDialog.vue';
import { useArrayData } from 'composables/useArrayData';
const { t } = useI18n();
const route = useRoute();
const props = defineProps({ const props = defineProps({
dataKey: { dataKey: {
type: String, type: String,
@ -27,13 +26,18 @@ const props = defineProps({
required: true, required: true,
}, },
}); });
const { t } = useI18n();
const route = useRoute();
const arrayData = useArrayData(props.dataKey);
const categoryList = ref(null); const categoryList = ref(null);
const selectedCategoryFk = ref(getParamWhere(route, 'categoryFk')); const selectedCategoryFk = ref(null);
const typeList = ref([]); const typeList = ref([]);
const selectedTypeFk = ref(null); const selectedTypeFk = ref(null);
const selectedTag = ref(null); const generalSearchParam = ref(null);
const tagValues = ref([{}]);
const tagOptions = ref([]);
const vnFilterPanelRef = ref(); const vnFilterPanelRef = ref();
const orderByList = ref([ const orderByList = ref([
{ id: 'relevancy DESC, name', name: t('params.relevancy'), priority: 999 }, { id: 'relevancy DESC, name', name: t('params.relevancy'), priority: 999 },
@ -48,32 +52,24 @@ const orderWayList = ref([
const orderBySelected = ref('relevancy DESC, name'); const orderBySelected = ref('relevancy DESC, name');
const orderWaySelected = ref('ASC'); const orderWaySelected = ref('ASC');
const createValue = (val, done) => { const resetCategory = (params, search) => {
if (val.length > 2) {
if (!tagOptions.value.includes(val)) {
done(tagOptions.value, 'add-unique');
}
tagValues.value.push({ value: val });
}
};
const resetCategory = () => {
selectedCategoryFk.value = null; selectedCategoryFk.value = null;
typeList.value = null; typeList.value = null;
}; params.categoryFk = null;
params.typeFk = null;
const clearFilter = (key) => { arrayData.store.userFilter = null;
if (key === 'categoryFk') { removeTagGroupParam(params, search);
resetCategory();
}
}; };
const selectCategory = (params, category, search) => { const selectCategory = (params, category, search) => {
if (params.categoryFk === category?.id) { if (params.categoryFk === category?.id) {
resetCategory(); resetCategory(params, search);
params.categoryFk = null; params.categoryFk = null;
} else { } else {
selectedCategoryFk.value = category?.id; selectedCategoryFk.value = category?.id;
params.categoryFk = category?.id; params.categoryFk = category?.id;
params.typeFk = null;
selectedTypeFk.value = null;
loadTypes(category?.id); loadTypes(category?.id);
} }
search(); search();
@ -86,19 +82,12 @@ const loadTypes = async (categoryFk = selectedCategoryFk.value) => {
typeList.value = data; typeList.value = data;
}; };
const selectedCategory = computed(() => const selectedCategory = computed(() => {
(categoryList.value || []).find( return (categoryList.value || []).find(
(category) => category?.id === selectedCategoryFk.value (category) => category?.id === selectedCategoryFk.value
) );
); });
function filterFn(val, update) {
update(() => {
const needle = val.toLowerCase();
tagOptions.value = props.tagValue.filter(
(v) => v.toLowerCase().indexOf(needle) > -1
);
});
}
const selectedType = computed(() => { const selectedType = computed(() => {
return (typeList.value || []).find((type) => type?.id === selectedTypeFk.value); return (typeList.value || []).find((type) => type?.id === selectedTypeFk.value);
}); });
@ -114,37 +103,26 @@ function exprBuilder(param, value) {
} }
} }
const applyTagFilter = (params, search) => { const applyTags = (tagInfo, params, search) => {
if (!tagValues.value?.length) { if (!tagInfo || !tagInfo.values.length) {
params.tagGroups = null; params.tagGroups = null;
search(); search();
return; return;
} }
if (!params.tagGroups) {
params.tagGroups = []; if (!params.tagGroups) params.tagGroups = [];
} params.tagGroups.push(tagInfo);
params.tagGroups.push(
JSON.stringify({
values: tagValues.value.filter((obj) => Object.keys(obj).length > 0),
tagSelection: {
...selectedTag.value,
orgShowField: selectedTag?.value?.name,
},
tagFk: selectedTag?.value?.tagFk,
})
);
search(); search();
selectedTag.value = null;
tagValues.value = [{}];
}; };
const removeTagChip = (selection, params, search) => { const removeTagGroupParam = (params, search, valIndex = null) => {
if (params.tagGroups) { if (!valIndex) {
params.tagGroups = (params.tagGroups || []).filter( params.tagGroups = null;
(value) => value !== selection search();
); } else {
params.tagGroups.splice(valIndex, 1);
search();
} }
search();
}; };
const setCategoryList = (data) => { const setCategoryList = (data) => {
@ -171,6 +149,11 @@ function addOrder(value, field, params) {
params.orderBy = JSON.stringify(orderBy); params.orderBy = JSON.stringify(orderBy);
vnFilterPanelRef.value.search(); vnFilterPanelRef.value.search();
} }
onMounted(() => {
selectedCategoryFk.value = getParamWhere(route, 'categoryFk');
selectedTypeFk.value = getParamWhere(route, 'typeFk');
});
</script> </script>
<template> <template>
@ -181,15 +164,11 @@ function addOrder(value, field, params) {
:hidden-tags="['orderFk', 'orderBy']" :hidden-tags="['orderFk', 'orderBy']"
:un-removable-params="['orderFk', 'orderBy']" :un-removable-params="['orderFk', 'orderBy']"
:expr-builder="exprBuilder" :expr-builder="exprBuilder"
:custom-tags="['tagGroups']" :custom-tags="['tagGroups', 'categoryFk']"
@remove="clearFilter"
:redirect="false" :redirect="false"
> >
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
<strong v-if="tag.label === 'categoryFk'"> <strong v-if="tag.label === 'typeFk'">
{{ t(selectedCategory?.name || '') }}
</strong>
<strong v-else-if="tag.label === 'typeFk'">
{{ t(selectedType?.name || '') }} {{ t(selectedType?.name || '') }}
</strong> </strong>
<div v-else class="q-gutter-x-xs"> <div v-else class="q-gutter-x-xs">
@ -198,22 +177,33 @@ function addOrder(value, field, params) {
</div> </div>
</template> </template>
<template #customTags="{ tags: customTags, params, searchFn }"> <template #customTags="{ tags: customTags, params, searchFn }">
<template v-for="tag in customTags" :key="tag.label"> <template v-for="customTag in customTags" :key="customTag.label">
<template v-if="tag.label === 'tagGroups'"> <VnFilterPanelChip
<VnFilterPanelChip v-for="(tag, valIndex) in Array.isArray(customTag.value)
v-for="chip in tag.value" ? customTag.value
:key="chip" : 1"
removable :key="valIndex"
@remove="removeTagChip(chip, params, searchFn)" removable
> @remove="
<strong> {{ JSON.parse(chip).tagSelection?.name }}: </strong> customTag.label === 'categoryFk'
<span>{{ ? resetCategory(params, searchFn)
(JSON.parse(chip).values || []) : removeTagGroupParam(params, searchFn, valIndex)
.map((item) => item.value) "
.join(' | ') >
}}</span> <strong v-if="customTag.label === 'categoryFk'">
</VnFilterPanelChip> {{ t(selectedCategory?.name || '') }}
</template> </strong>
<strong v-if="tag?.tagSelection?.name" class="q-mr-xs">
{{ tag.tagSelection.name }}:
</strong>
<span>
{{
(tag?.values || [])
.map((item) => `"${item.value}"`)
.join(', ')
}}
</span>
</VnFilterPanelChip>
</template> </template>
</template> </template>
<template #body="{ params, searchFn }"> <template #body="{ params, searchFn }">
@ -297,91 +287,46 @@ function addOrder(value, field, params) {
</QItemSection> </QItemSection>
</QItem> </QItem>
<QSeparator /> <QSeparator />
<QItem class="q-mt-md">
<QItemSection>
<VnSelect
:label="t('params.tag')"
v-model="selectedTag"
:options="props.tags || []"
option-value="id"
option-label="name"
dense
outlined
rounded
:emit-value="false"
use-input
/>
</QItemSection>
</QItem>
<QItem
v-for="(value, index) in tagValues"
:key="value"
class="q-mt-md filter-value"
>
<FetchData
v-if="selectedTag"
:url="`Tags/${selectedTag}/filterValue`"
limit="30"
auto-load
@on-fetch="(data) => (tagOptions = data)"
/>
<VnSelect
v-if="!selectedTag"
:label="t('params.value')"
v-model="value.value"
:options="tagOptions || []"
option-value="value"
option-label="value"
dense
outlined
rounded
emit-value
use-input
class="filter-input"
@new-value="createValue"
@filter="filterFn"
@update:model-value="applyTagFilter(params, searchFn)"
/>
<VnSelect
v-else-if="selectedTag === 1"
:label="t('params.value')"
v-model="value.value"
:options="tagOptions || []"
option-value="value"
option-label="value"
dense
outlined
rounded
emit-value
use-input
class="filter-input"
@new-value="createValue"
@update:model-value="applyTagFilter(params, searchFn)"
/>
<VnInput
v-else
:label="t('params.value')"
v-model="value.value"
dense
outlined
rounded
class="filter-input"
@keyup.enter="applyTagFilter(params, searchFn)"
/>
<QIcon
name="delete"
class="filter-icon"
@click="(tagValues || []).splice(index, 1)"
/>
</QItem>
<QItem class="q-mt-lg"> <QItem class="q-mt-lg">
<QBtn <VnSelect
icon="add_circle" :label="t('components.itemsFilterPanel.value')"
shortcut="+" :options="props.tagValue"
flat dense
class="filter-icon" outlined
@click="tagValues.push({})" rounded
/> :is-clearable="false"
v-model="generalSearchParam"
@update:model-value="
applyTags(
{ values: [{ value: generalSearchParam }] },
params,
searchFn
)
"
>
<template #prepend>
<QIcon name="search" />
</template>
<template #after>
<QBtn
icon="add_circle"
shortcut="+"
flat
color="primary"
size="md"
/>
<QPopupProxy>
<CatalogFilterValueDialog
style="display: inline-block"
:tags="tags"
@apply-tags="
($event) => applyTags($event, params, searchFn)
"
/>
</QPopupProxy>
</template>
</VnSelect>
</QItem> </QItem>
<QSeparator /> <QSeparator />
</template> </template>
@ -416,23 +361,6 @@ function addOrder(value, field, params) {
cursor: pointer; cursor: pointer;
} }
} }
.filter-icon {
font-size: 24px;
color: $primary;
padding: 0 4px;
cursor: pointer;
}
.filter-input {
flex-shrink: 1;
min-width: 0;
}
.filter-value {
display: flex;
align-items: center;
}
</style> </style>
<i18n> <i18n>