From c24480099f0ad3d30a8eed19882c90f8640ad6b6 Mon Sep 17 00:00:00 2001 From: wbuezas <wbuezas@verdnatura.es> Date: Tue, 18 Mar 2025 14:32:00 -0300 Subject: [PATCH] Create new searchbar and UsersView requests --- src/components/ui/NewVnSearchBar.vue | 120 +++++++++++++++++++++++++++ src/composables/serviceUtils.js | 116 ++++++++++++++++++++++++++ src/pages/Admin/UsersView.vue | 37 ++++++--- 3 files changed, 262 insertions(+), 11 deletions(-) create mode 100644 src/components/ui/NewVnSearchBar.vue create mode 100644 src/composables/serviceUtils.js diff --git a/src/components/ui/NewVnSearchBar.vue b/src/components/ui/NewVnSearchBar.vue new file mode 100644 index 00000000..d4abcec7 --- /dev/null +++ b/src/components/ui/NewVnSearchBar.vue @@ -0,0 +1,120 @@ +<script setup> +import { onMounted, ref } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useRoute, useRouter } from 'vue-router'; + +import VnInput from 'src/components/common/VnInput.vue'; + +import { fetch } from 'src/composables/serviceUtils'; + +const props = defineProps({ + searchFn: { + type: Function, + default: null + }, + placeholder: { + type: String, + default: '' + }, + url: { + type: String, + default: null + }, + filter: { + type: String, + default: null + }, + searchField: { + type: String, + default: 'search' + }, + exprBuilder: { + type: Function, + default: null + } +}); + +const emit = defineEmits(['onSearch', 'onSearchError']); + +const { t } = useI18n(); +const router = useRouter(); +const route = useRoute(); + +const searchTerm = ref(''); + +const search = async () => { + try { + router.replace({ + query: searchTerm.value ? { search: searchTerm.value } : {} + }); + + if (!searchTerm.value) { + emit('onSearchError'); + return; + } + + if (props.url) { + const params = { + filter: props.filter, + [props.searchField]: searchTerm.value + }; + + const { data } = await fetch({ + url: props.url, + params, + exprBuilder: props.exprBuilder + }); + + emit('onSearch', data); + } + } catch (error) { + console.error('Error searching:', error); + emit('onSearchError'); + } +}; + +onMounted(() => { + if (route.query.search) { + searchTerm.value = route.query.search; + search(); + } +}); +</script> +<template> + <VnInput + v-model="searchTerm" + @keyup.enter="search()" + :placeholder="props.placeholder || t('search')" + bg-color="white" + is-outlined + :clearable="false" + class="searchbar" + data-cy="searchBar" + > + <template #prepend> + <QIcon name="search" class="cursor-pointer" @click="search()" /> + </template> + </VnInput> +</template> + +<style lang="scss" scoped> +@import 'src/css/responsive'; + +.searchbar { + @include mobile { + max-width: 120px; + } +} +</style> +<i18n lang="yaml"> +en-US: + search: Search +es-ES: + search: Buscar +ca-ES: + search: Cercar +fr-FR: + search: Rechercher +pt-PT: + search: Pesquisar +</i18n> diff --git a/src/composables/serviceUtils.js b/src/composables/serviceUtils.js new file mode 100644 index 00000000..ce124398 --- /dev/null +++ b/src/composables/serviceUtils.js @@ -0,0 +1,116 @@ +import { api } from '@/boot/axios'; + +function buildFilter(params, builderFunc) { + let and = []; + + for (let param in params) { + let value = params[param]; + if (value == null) continue; + let expr = builderFunc(param, value); + if (expr) and.push(expr); + } + return simplifyOperation(and, 'and'); +} + +const simplifyOperation = (operation, operator) => { + switch (operation.length) { + case 0: + return undefined; + case 1: + return operation[0]; + default: + return { [operator]: operation }; + } +}; + +async function fetch({ + url, + append = false, + params, + exprBuilder, + mapKey, + existingData = [], + existingMap = new Map(), + oneRecord = false +}) { + if (!url) return; + + let exprFilter; + if (exprBuilder) { + exprFilter = buildFilter(params, (param, value) => { + if (param === 'filter') return; + const res = exprBuilder(param, value); + if (res) delete params[param]; + return res; + }); + } + + if (params.filter?.where || exprFilter) { + params.filter.where = { ...params.filter.where, ...exprFilter }; + } + + if (!params?.filter?.order?.length) { + delete params?.filter?.order; + } + + params.filter = JSON.stringify(params.filter); + + const response = await api.get(url, { params }); + const processedData = processData(response.data, { + mapKey, + map: !!mapKey, + append, + oneRecord, + existingData, + existingMap + }); + + return { response, data: processedData.data, map: processedData.map }; +} + +function processData(data, options) { + const { + mapKey, + map = true, + append = true, + oneRecord = false, + existingData = [], + existingMap = new Map() + } = options; + let resultData = [...existingData]; + let resultMap = new Map(existingMap); + + if (oneRecord) { + return Array.isArray(data) ? data[0] : data; + } + + if (!append) { + resultData = []; + resultMap = new Map(); + } + + if (!Array.isArray(data)) { + resultData = data; + } else if (!map && append) { + resultData.push(...data); + } else { + for (const row of data) { + const key = row[mapKey]; + const val = { ...row, key }; + if (key && resultMap.has(key)) { + const { position } = resultMap.get(key); + val.position = position; + resultMap.set(key, val); + resultData[position] = val; + } else { + val.position = resultMap.size; + resultMap.set(key, val); + resultData.push(val); + } + } + } + + return { data: resultData, map: resultMap }; +} + +export { buildFilter, fetch, processData }; diff --git a/src/pages/Admin/UsersView.vue b/src/pages/Admin/UsersView.vue index 33de8f70..c72167f2 100644 --- a/src/pages/Admin/UsersView.vue +++ b/src/pages/Admin/UsersView.vue @@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; import CardList from 'src/components/ui/CardList.vue'; -import VnSearchBar from 'src/components/ui/VnSearchBar.vue'; +import VnSearchBar from 'src/components/ui/NewVnSearchBar.vue'; import VnList from 'src/components/ui/VnList.vue'; import { useAppStore } from 'stores/app'; @@ -16,16 +16,11 @@ const router = useRouter(); const userStore = useUserStore(); const appStore = useAppStore(); const { isHeaderMounted } = storeToRefs(appStore); - const loading = ref(false); const users = ref([]); - -const query = `SELECT u.id, u.name, u.nickname, u.active - FROM account.user u - WHERE u.name LIKE CONCAT('%', #user, '%') - OR u.nickname LIKE CONCAT('%', #user, '%') - OR u.id = #user - ORDER BY u.name LIMIT 200`; +const filter = { + fields: ['id', 'name', 'nickname', 'active'] +}; const onSearch = data => (users.value = data || []); @@ -38,15 +33,35 @@ const supplantUser = async user => { console.error('Error supplanting user:', error); } }; + +const usersExprBuilder = (param, value) => { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? { id: value } + : { + or: [ + { name: { like: `%${value}%` } }, + { nickname: { like: `%${value}%` } } + ] + }; + case 'name': + case 'nickname': + return { [param]: { like: `%${value}%` } }; + case 'roleFk': + return { [param]: value }; + } +}; </script> <template> <Teleport v-if="isHeaderMounted" to="#actions"> <VnSearchBar - :sql-query="query" - search-field="user" + url="/VnUsers/preview" @on-search="onSearch" @on-search-error="users = []" + :expr-builder="usersExprBuilder" + :filter="filter" data-cy="usersViewSearchBar" /> </Teleport>