diff --git a/src/composables/arrayDataCore.js b/src/composables/arrayDataCore.js new file mode 100644 index 000000000..3f2f7a7cd --- /dev/null +++ b/src/composables/arrayDataCore.js @@ -0,0 +1,384 @@ +import { onMounted, computed } from 'vue'; +import { useRouter, useRoute } from 'vue-router'; +import axios from 'axios'; +import { useArrayDataStore } from 'stores/useArrayDataStore'; +import { buildFilter } from 'filters/filterPanel'; +import { isDialogOpened } from 'src/filters'; + +const arrayDataStore = useArrayDataStore(); + +export function arrayDataCore(key, userOptions) { + let route = null; + let router = null; + + // Si no hay key, intentamos obtenerla del route + if (!key) { + key = initialRoute?.meta?.moduleName; + route = initialRoute; + router = initialRouter; + } + if (!key) throw new Error('ArrayData: A key is required to use this composable'); + + if (!arrayDataStore.get(key)) arrayDataStore.set(key); + + const store = arrayDataStore.get(key); + let canceller = null; + + const { route: initialRoute, router: initialRouter } = (() => { + if (!route) route = useRoute(); + if (!router) router = useRouter(); + return { route, router }; + })(); + function setupMountedHook() { + setOptions(); + reset(['skip']); + route = initialRoute; + router = initialRouter; + const searchUrl = store.searchUrl; + const query = route.query[searchUrl]; + if (query) { + const params = JSON.parse(query); + const filter = + params?.filter && typeof params?.filter == 'object' + ? params?.filter + : JSON.parse(params?.filter ?? '{}'); + delete params.filter; + + store.userParams = params; + store.filter = { ...filter, ...store.userFilter }; + if (filter?.order) store.order = filter.order; + } + setCurrentFilter(); + } + if (key && userOptions) setOptions(); + + function setOptions() { + const allowedOptions = [ + 'url', + 'filter', + 'where', + 'order', + 'limit', + 'skip', + 'userParams', + 'userFilter', + 'exprBuilder', + 'searchUrl', + 'navigate', + 'mapKey', + 'keepData', + 'oneRecord', + ]; + if (typeof userOptions === 'object') { + for (const option in userOptions) { + const isEmpty = userOptions[option] == null || userOptions[option] === ''; + if (isEmpty || !allowedOptions.includes(option)) continue; + + if (Object.hasOwn(store, option)) { + const defaultOpts = userOptions[option]; + store[option] = userOptions.keepOpts?.includes(option) + ? Object.assign(defaultOpts, store[option]) + : defaultOpts; + if (option === 'userParams') store.defaultParams = store[option]; + } + } + } + } + + async function fetch({ append = false, updateRouter = true }) { + if (!store.url) return; + + cancelRequest(); + canceller = new AbortController(); + const { params, limit } = setCurrentFilter(); + + let exprFilter; + if (store?.exprBuilder) { + exprFilter = buildFilter(params, (param, value) => { + if (param == 'filter') return; + const res = store.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); + + store.isLoading = true; + const response = await axios.get(store.url, { + signal: canceller.signal, + params, + }); + + store.hasMoreData = limit && response.data.length >= limit; + + if (!append && !isDialogOpened() && updateRouter) { + if (updateStateParams(response.data)?.redirect && !store.keepData) return; + } + store.isLoading = false; + canceller = null; + + processData(response.data, { + map: !!store.mapKey, + append, + oneRecord: store.oneRecord, + }); + + return response; + } + + function destroy() { + if (arrayDataStore.get(key)) { + arrayDataStore.clear(key); + } + } + + function deleteOption(option) { + delete store[option]; + } + + function reset(opts = []) { + if (arrayDataStore.get(key)) arrayDataStore.reset(key, opts); + } + + function resetPagination() { + if (arrayDataStore.get(key)) arrayDataStore.resetPagination(key); + } + + function cancelRequest() { + if (canceller) { + canceller.abort(); + canceller = null; + } + } + + async function applyFilter({ filter, params }, fetchOptions = {}) { + if (filter) store.filter = filter; + if (params) store.userParams = { ...params }; + + const response = await fetch(fetchOptions); + return response; + } + + async function addFilter({ filter, params }) { + if (filter) store.filter = filter; + + let userParams = { ...store.userParams, ...params }; + userParams = sanitizerParams(userParams, store?.exprBuilder); + + store.userParams = userParams; + resetPagination(); + + await fetch({}); + return { filter, params }; + } + + async function addFilterWhere(where) { + const storedFilter = { ...store.filter }; + if (!storedFilter?.where) storedFilter.where = {}; + where = { ...storedFilter.where, ...where }; + await addFilter({ filter: { where } }); + } + + async function addOrder(field, direction = 'ASC') { + const newOrder = field + ' ' + direction; + const order = toArray(store.order); + + let index = getOrderIndex(order, field); + if (index > -1) { + order[index] = newOrder; + } else { + index = order.length; + order.push(newOrder); + } + + store.order = order; + resetPagination(); + fetch({}); + index++; + + return { index, order }; + } + + async function deleteOrder(field) { + const order = toArray(store.order); + const index = getOrderIndex(order, field); + if (index > -1) order.splice(index, 1); + + store.order = order; + fetch({}); + } + + function getOrderIndex(order, field) { + return order.findIndex((o) => o.split(' ')[0] === field); + } + + function toArray(str = []) { + if (!str) return []; + if (Array.isArray(str)) return str; + if (typeof str === 'string') return str.split(',').map((item) => item.trim()); + } + + function sanitizerParams(params, exprBuilder) { + for (const param in params) { + if (params[param] === '' || params[param] === null) { + delete store.userParams[param]; + delete params[param]; + if (store.filter?.where) { + let key; + if (exprBuilder) { + const result = exprBuilder(param); + if (result !== undefined && result !== null) + key = Object.keys(result); + } else { + if (typeof param === 'object' && param !== null) + key = Object.keys(param); + } + if (key && key[0]) { + delete store.filter.where[key[0]]; + if (Object.keys(store.filter.where).length === 0) { + delete store.filter.where; + } + } + } + } + } + return params; + } + + async function loadMore() { + if (!store.hasMoreData) return; + + store.skip = (store?.filter?.limit ?? store.limit) * store.page; + store.page += 1; + + await fetch({ append: true }); + } + + async function refresh() { + if (Object.values(store.userParams).length) await fetch({}); + } + + function updateStateParams(data) { + if (!route?.path) return; + const newUrl = { path: route.path, query: { ...(route.query ?? {}) } }; + if (store?.searchUrl) + newUrl.query[store.searchUrl] = JSON.stringify(store.currentFilter); + + 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 = + data?.length === 1 + ? path.replace(/\/(list|:id)|-list/, `/${data[0].id}`) + : path.replace(/:id.*/, ''); + + if (route.path != to) { + const pushUrl = { path: to }; + if (to.endsWith('/list') || to.endsWith('/')) + pushUrl.query = newUrl.query; + return router.push(pushUrl) && { redirect: true }; + } + } + + router.replace(newUrl); + } + + function getCurrentFilter() { + if (!Object.keys(store.userParams).length) + store.userParams = store.defaultParams ?? {}; + + const filter = { + limit: store.limit, + ...store.userFilter, + }; + + let where; + if (filter?.where || store.filter?.where) + where = Object.assign(filter?.where ?? {}, store.filter?.where ?? {}); + Object.assign(filter, store.filter); + filter.where = where; + const params = { filter }; + + Object.assign(params, store.userParams); + if (params.filter) params.filter.skip = store.skip; + if (store.order) params.filter.order = toArray(store.order); + else delete params.filter.order; + + return { filter, params, limit: filter.limit }; + } + + function setCurrentFilter() { + const { params, limit } = getCurrentFilter(); + store.currentFilter = JSON.parse(JSON.stringify(params)); + delete store.currentFilter.filter.include; + store.currentFilter.filter = JSON.stringify(store.currentFilter.filter); + return { params, limit }; + } + + function processData(data, { map = true, append = true, oneRecord = false }) { + if (oneRecord) { + store.data = Array.isArray(data) ? data[0] : data; + return; + } + if (!append) { + store.data = []; + store.map = new Map(); + } + + if (!Array.isArray(data)) store.data = data; + else if (!map && append) for (const row of data) store.data.push(row); + else + for (const row of data) { + const key = row[store.mapKey]; + const val = { ...row, key }; + if (key && store.map.has(key)) { + const { position } = store.map.get(key); + val.position = position; + store.map.set(key, val); + store.data[position] = val; + } else { + val.position = store.map.size; + store.map.set(key, val); + store.data.push(val); + } + } + } + + const totalRows = computed(() => (store.data && store.data.length) || 0); + const isLoading = computed(() => store.isLoading || false); + + return { + setupMountedHook, + fetch, + applyFilter, + addFilter, + getCurrentFilter, + setCurrentFilter, + addFilterWhere, + addOrder, + deleteOrder, + refresh, + destroy, + loadMore, + store, + totalRows, + updateStateParams, + isLoading, + deleteOption, + reset, + resetPagination, + }; +} diff --git a/src/composables/useArrayData.js b/src/composables/useArrayData.js index d1c1b01b8..969fc0f8c 100644 --- a/src/composables/useArrayData.js +++ b/src/composables/useArrayData.js @@ -1,373 +1,7 @@ -import { onMounted, computed } from 'vue'; -import { useRouter, useRoute } from 'vue-router'; -import axios from 'axios'; -import { useArrayDataStore } from 'stores/useArrayDataStore'; -import { buildFilter } from 'filters/filterPanel'; -import { isDialogOpened } from 'src/filters'; +import { arrayDataCore } from './arrayDataCore'; -const arrayDataStore = useArrayDataStore(); - -export function useArrayData(key, userOptions) { - key ??= useRoute().meta.moduleName; - - if (!key) throw new Error('ArrayData: A key is required to use this composable'); - - if (!arrayDataStore.get(key)) arrayDataStore.set(key); - - const store = arrayDataStore.get(key); - const route = useRoute(); - const router = useRouter(); - let canceller = null; - - onMounted(() => { - setOptions(); - reset(['skip']); - - const query = route.query; - const searchUrl = store.searchUrl; - if (query[searchUrl]) { - const params = JSON.parse(query[searchUrl]); - const filter = - params?.filter && typeof params?.filter == 'object' - ? params?.filter - : JSON.parse(params?.filter ?? '{}'); - delete params.filter; - - store.userParams = params; - store.filter = { ...filter, ...store.userFilter }; - if (filter?.order) store.order = filter.order; - } - setCurrentFilter(); - }); - - if (key && userOptions) setOptions(); - - function setOptions() { - const allowedOptions = [ - 'url', - 'filter', - 'where', - 'order', - 'limit', - 'skip', - 'userParams', - 'userFilter', - 'exprBuilder', - 'searchUrl', - 'navigate', - 'mapKey', - 'keepData', - 'oneRecord', - ]; - if (typeof userOptions === 'object') { - for (const option in userOptions) { - const isEmpty = userOptions[option] == null || userOptions[option] === ''; - if (isEmpty || !allowedOptions.includes(option)) continue; - - if (Object.hasOwn(store, option)) { - const defaultOpts = userOptions[option]; - store[option] = userOptions.keepOpts?.includes(option) - ? Object.assign(defaultOpts, store[option]) - : defaultOpts; - if (option === 'userParams') store.defaultParams = store[option]; - } - } - } - } - - async function fetch({ append = false, updateRouter = true }) { - if (!store.url) return; - - cancelRequest(); - canceller = new AbortController(); - const { params, limit } = setCurrentFilter(); - - let exprFilter; - if (store?.exprBuilder) { - exprFilter = buildFilter(params, (param, value) => { - if (param == 'filter') return; - const res = store.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); - - store.isLoading = true; - const response = await axios.get(store.url, { - signal: canceller.signal, - params, - }); - - store.hasMoreData = limit && response.data.length >= limit; - - if (!append && !isDialogOpened() && updateRouter) { - if (updateStateParams(response.data)?.redirect && !store.keepData) return; - } - store.isLoading = false; - canceller = null; - - processData(response.data, { - map: !!store.mapKey, - append, - oneRecord: store.oneRecord, - }); - - return response; - } - - function destroy() { - if (arrayDataStore.get(key)) { - arrayDataStore.clear(key); - } - } - - function deleteOption(option) { - delete store[option]; - } - - function reset(opts = []) { - if (arrayDataStore.get(key)) arrayDataStore.reset(key, opts); - } - - function resetPagination() { - if (arrayDataStore.get(key)) arrayDataStore.resetPagination(key); - } - - function cancelRequest() { - if (canceller) { - canceller.abort(); - canceller = null; - } - } - - async function applyFilter({ filter, params }, fetchOptions = {}) { - if (filter) store.filter = filter; - if (params) store.userParams = { ...params }; - - const response = await fetch(fetchOptions); - return response; - } - - async function addFilter({ filter, params }) { - if (filter) store.filter = filter; - - let userParams = { ...store.userParams, ...params }; - userParams = sanitizerParams(userParams, store?.exprBuilder); - - store.userParams = userParams; - resetPagination(); - - await fetch({}); - return { filter, params }; - } - - async function addFilterWhere(where) { - const storedFilter = { ...store.filter }; - if (!storedFilter?.where) storedFilter.where = {}; - where = { ...storedFilter.where, ...where }; - await addFilter({ filter: { where } }); - } - - async function addOrder(field, direction = 'ASC') { - const newOrder = field + ' ' + direction; - const order = toArray(store.order); - - let index = getOrderIndex(order, field); - if (index > -1) { - order[index] = newOrder; - } else { - index = order.length; - order.push(newOrder); - } - - store.order = order; - resetPagination(); - fetch({}); - index++; - - return { index, order }; - } - - async function deleteOrder(field) { - const order = toArray(store.order); - const index = getOrderIndex(order, field); - if (index > -1) order.splice(index, 1); - - store.order = order; - fetch({}); - } - - function getOrderIndex(order, field) { - return order.findIndex((o) => o.split(' ')[0] === field); - } - - function toArray(str = []) { - if (!str) return []; - if (Array.isArray(str)) return str; - if (typeof str === 'string') return str.split(',').map((item) => item.trim()); - } - - function sanitizerParams(params, exprBuilder) { - for (const param in params) { - if (params[param] === '' || params[param] === null) { - delete store.userParams[param]; - delete params[param]; - if (store.filter?.where) { - let key; - if (exprBuilder) { - const result = exprBuilder(param); - if (result !== undefined && result !== null) - key = Object.keys(result); - } else { - if (typeof param === 'object' && param !== null) - key = Object.keys(param); - } - if (key && key[0]) { - delete store.filter.where[key[0]]; - if (Object.keys(store.filter.where).length === 0) { - delete store.filter.where; - } - } - } - } - } - return params; - } - - async function loadMore() { - if (!store.hasMoreData) return; - - store.skip = (store?.filter?.limit ?? store.limit) * store.page; - store.page += 1; - - await fetch({ append: true }); - } - - async function refresh() { - if (Object.values(store.userParams).length) await fetch({}); - } - - function updateStateParams(data) { - if (!route?.path) return; - const newUrl = { path: route.path, query: { ...(route.query ?? {}) } }; - if (store?.searchUrl) - newUrl.query[store.searchUrl] = JSON.stringify(store.currentFilter); - - 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 = - data?.length === 1 - ? path.replace(/\/(list|:id)|-list/, `/${data[0].id}`) - : path.replace(/:id.*/, ''); - - if (route.path != to) { - const pushUrl = { path: to }; - if (to.endsWith('/list') || to.endsWith('/')) - pushUrl.query = newUrl.query; - return router.push(pushUrl) && { redirect: true }; - } - } - - router.replace(newUrl); - } - - function getCurrentFilter() { - if (!Object.keys(store.userParams).length) - store.userParams = store.defaultParams ?? {}; - - const filter = { - limit: store.limit, - ...store.userFilter, - }; - - let where; - if (filter?.where || store.filter?.where) - where = Object.assign(filter?.where ?? {}, store.filter?.where ?? {}); - Object.assign(filter, store.filter); - filter.where = where; - const params = { filter }; - - Object.assign(params, store.userParams); - if (params.filter) params.filter.skip = store.skip; - if (store.order) params.filter.order = toArray(store.order); - else delete params.filter.order; - - return { filter, params, limit: filter.limit }; - } - - function setCurrentFilter() { - const { params, limit } = getCurrentFilter(); - store.currentFilter = JSON.parse(JSON.stringify(params)); - delete store.currentFilter.filter.include; - store.currentFilter.filter = JSON.stringify(store.currentFilter.filter); - return { params, limit }; - } - - function processData(data, { map = true, append = true, oneRecord = false }) { - if (oneRecord) { - store.data = Array.isArray(data) ? data[0] : data; - return; - } - if (!append) { - store.data = []; - store.map = new Map(); - } - - if (!Array.isArray(data)) store.data = data; - else if (!map && append) for (const row of data) store.data.push(row); - else - for (const row of data) { - const key = row[store.mapKey]; - const val = { ...row, key }; - if (key && store.map.has(key)) { - const { position } = store.map.get(key); - val.position = position; - store.map.set(key, val); - store.data[position] = val; - } else { - val.position = store.map.size; - store.map.set(key, val); - store.data.push(val); - } - } - } - - const totalRows = computed(() => (store.data && store.data.length) || 0); - const isLoading = computed(() => store.isLoading || false); - - return { - fetch, - applyFilter, - addFilter, - getCurrentFilter, - setCurrentFilter, - addFilterWhere, - addOrder, - deleteOrder, - refresh, - destroy, - loadMore, - store, - totalRows, - updateStateParams, - isLoading, - deleteOption, - reset, - resetPagination, - }; +export function useArrayData(key, options) { + const arrayData = arrayDataCore(key, options); + arrayData.setupMountedHook(); + return arrayData; }