import { type ModelRef, type Ref, watch, nextTick } from "vue"; import { Notify } from "quasar"; import { KettalVisibleColumn, KettalTableAdvancedFilters, KettalQTableColumn, } from "@/types/KTable"; const COLS_QUERY_KEY = "cols"; const MAX_URL_LENGTH = 1200; const TOO_MANY_FILTERS_MESSAGE = "Demasiados filtros aplicados. Se han eliminado los filtros y las columnas de la URL."; function encodeColumns(cols: KettalVisibleColumn[]): string { return cols.map((c) => (c.isVisible ? "" : "!") + c.name).join("."); } function decodeColumns(encoded: string): KettalVisibleColumn[] | null { try { return encoded .split(".") .filter(Boolean) .map((part) => { if (part.startsWith("!")) { return { name: part.slice(1), isVisible: false }; } return { name: part, isVisible: true }; }); } catch { return null; } } function getQueryParams(): URLSearchParams { return new URLSearchParams(window.location.search); } function buildUrl(params: URLSearchParams): string { return params.toString() ? `${window.location.pathname}?${params.toString()}` : window.location.pathname; } function updateQueryParams( params: URLSearchParams, options: { enforceMaxLength?: boolean } = {}, ): boolean { const { enforceMaxLength = true } = options; const newUrl = params.toString() ? `${window.location.pathname}?${params.toString()}` : window.location.pathname; if (enforceMaxLength && newUrl.length > MAX_URL_LENGTH) { return false; } history.replaceState(history.state, "", newUrl); return true; } export default function useUrlSync( syncColumns: boolean, syncFilters: boolean, visibleColumns: ModelRef, advancedFilters: Ref, columns: KettalQTableColumn[], ) { if (typeof window === "undefined" || (!syncColumns && !syncFilters)) return; let isUrlSyncing = false; let isHandlingOverflow = false; function showTooManyFiltersMessage() { Notify.create({ type: "warning", message: TOO_MANY_FILTERS_MESSAGE, position: "top", timeout: 3500, }); } function resetColumnsAndFiltersFromUrl() { if (isHandlingOverflow) return; isHandlingOverflow = true; isUrlSyncing = true; visibleColumns.value = columns.map((c) => ({ name: c.name, isVisible: true, })); advancedFilters.value = {}; const cleanedParams = getQueryParams(); cleanedParams.delete(COLS_QUERY_KEY); columns.forEach((col) => cleanedParams.delete(col.name)); // Forzar limpieza de parámetros de tabla incluso si la URL ya era larga. updateQueryParams(cleanedParams, { enforceMaxLength: false }); nextTick(() => { isUrlSyncing = false; isHandlingOverflow = false; }); showTooManyFiltersMessage(); } function shouldResetByUrlLength(params: URLSearchParams): boolean { return buildUrl(params).length > MAX_URL_LENGTH; } // --- Column sync --- function readColumnsFromUrl() { const params = getQueryParams(); const encoded = params.get(COLS_QUERY_KEY); if (!encoded) return; const decoded = decodeColumns(encoded); if (!decoded || decoded.length === 0) return; const validNames = new Set(columns.map((c) => c.name)); // 1. Filtrar solo columnas válidas (manteniendo orden URL) const filtered = decoded.filter((col) => validNames.has(col.name)); // 2. Añadir columnas que falten (por defecto visibles) const decodedNames = new Set(filtered.map((c) => c.name)); const missing = columns .filter((c) => !decodedNames.has(c.name)) .map((c) => ({ name: c.name, isVisible: true })); const finalCols = [...filtered, ...missing]; isUrlSyncing = true; visibleColumns.value = finalCols; nextTick(() => { isUrlSyncing = false; }); } function writeColumnsToUrl(cols: KettalVisibleColumn[]) { const params = getQueryParams(); const current = params.get(COLS_QUERY_KEY); const next = encodeColumns(cols); if (current === next) return; params.set(COLS_QUERY_KEY, next); if (shouldResetByUrlLength(params) || !updateQueryParams(params)) { resetColumnsAndFiltersFromUrl(); } } // --- Filter sync --- function readFiltersFromUrl() { const params = getQueryParams(); const filters: KettalTableAdvancedFilters = {}; const processedKeys = new Set(); params.forEach((_, key) => { if (key === COLS_QUERY_KEY || processedKeys.has(key)) return; processedKeys.add(key); const allValues = params.getAll(key); if (allValues.length > 1) { filters[key] = allValues; } else { const value = allValues[0]; if (value === "true" || value === "false") { filters[key] = value === "true"; } else { const column = columns.find((col) => col.name === key); if ( column && (column.type === "select" || column.type === "option") ) { filters[key] = [value]; } else { filters[key] = value; } } } }); if (Object.keys(filters).length > 0) { isUrlSyncing = true; for (const [key, value] of Object.entries(filters)) { advancedFilters.value[key] = value; } nextTick(() => { isUrlSyncing = false; }); } } function writeFiltersToUrl(filters: KettalTableAdvancedFilters) { const currentParams = new URLSearchParams(window.location.search); const newParams = new URLSearchParams(); const currentCols = currentParams.get(COLS_QUERY_KEY); if (currentCols) newParams.set(COLS_QUERY_KEY, currentCols); for (const [key, value] of Object.entries(filters)) { if (Array.isArray(value)) { if (value.length > 0) value.forEach((v) => newParams.append(key, v)); } else if (typeof value === "boolean") { newParams.set(key, String(value)); } else if ( value !== null && value !== "null" && typeof value === "string" && value.trim() !== "" ) { newParams.set(key, value); } } if (newParams.toString() === currentParams.toString()) return; if (shouldResetByUrlLength(newParams) || !updateQueryParams(newParams)) { resetColumnsAndFiltersFromUrl(); } } // --- Setup watches --- if (syncColumns) { readColumnsFromUrl(); watch( visibleColumns, (newCols) => { if (!isUrlSyncing) writeColumnsToUrl(newCols); }, { deep: true }, ); } if (syncFilters) { readFiltersFromUrl(); watch( advancedFilters, (newFilters) => { if (!isUrlSyncing) writeFiltersToUrl(newFilters); }, { deep: true }, ); } function handlePopState() { if (syncColumns) readColumnsFromUrl(); if (syncFilters) readFiltersFromUrl(); } window.addEventListener("popstate", handlePopState); }