import { createContext, useCallback, useContext, useMemo, useState } from 'react' import { useSearchParams } from 'react-router-dom' import PropTypes from 'prop-types' const TableStateContext = createContext() export const FILTER_URL_PARAM = 'filter' export const SORT_URL_PARAM = 'sort' const getSessionFilterKey = (scope) => `tableState:${scope}:filter` const getSessionSortKey = (scope) => `tableState:${scope}:sort` // eslint-disable-next-line react-refresh/only-export-components export const getActiveFilterValues = (filterState) => { const active = {} Object.entries(filterState || {}).forEach(([k, v]) => { if (v !== '' && v !== undefined) active[k] = v }) return active } const parseJsonParam = (raw) => { if (!raw) return null try { const parsed = JSON.parse(raw) return typeof parsed === 'object' && parsed !== null ? parsed : null } catch { return null } } const readFilterFromUrl = (searchParams) => parseJsonParam(searchParams.get(FILTER_URL_PARAM)) const readSortFromUrl = (searchParams) => { const parsed = parseJsonParam(searchParams.get(SORT_URL_PARAM)) if (!parsed?.field || !parsed?.order) return null return { field: parsed.field, order: parsed.order } } const readFilterFromSession = (scope) => { try { return parseJsonParam(sessionStorage.getItem(getSessionFilterKey(scope))) } catch { return null } } // eslint-disable-next-line react-refresh/only-export-components export const hasStoredSessionFilter = (scope) => { if (!scope) return false return Object.keys(getActiveFilterValues(readFilterFromSession(scope) || {})).length > 0 } const readSortFromSession = (scope) => { try { const parsed = parseJsonParam(sessionStorage.getItem(getSessionSortKey(scope))) if (!parsed?.field || !parsed?.order) return null return { field: parsed.field, order: parsed.order } } catch { return null } } const writeFilterToSession = (scope, filter) => { const key = getSessionFilterKey(scope) if (Object.keys(filter).length > 0) { sessionStorage.setItem(key, JSON.stringify(filter)) } else { sessionStorage.removeItem(key) } } const writeSortToSession = (scope, sorter) => { const key = getSessionSortKey(scope) if (sorter?.field && sorter?.order) { sessionStorage.setItem(key, JSON.stringify(sorter)) } else { sessionStorage.removeItem(key) } } const normalizeSorter = (sorter) => { if (sorter?.field && sorter?.order) { return { field: sorter.field, order: sorter.order } } return {} } export const TableStateProvider = ({ children }) => { const [searchParams, setSearchParams] = useSearchParams() const [pageFilters, setPageFilters] = useState({}) const [pageSorters, setPageSorters] = useState({}) const getPersistedFilter = useCallback( (scope, { useFilterInUrl = false, useFilterInSession = false } = {}) => { if (useFilterInUrl) { const fromUrl = readFilterFromUrl(searchParams) if (fromUrl) return fromUrl } if (useFilterInSession) { const fromSession = readFilterFromSession(scope) if (fromSession) return fromSession } return {} }, [searchParams] ) const getPersistedSorter = useCallback( (scope, { useSortInUrl = false, useSortInSession = false } = {}) => { if (useSortInUrl) { const fromUrl = readSortFromUrl(searchParams) if (fromUrl) return fromUrl } if (useSortInSession) { const fromSession = readSortFromSession(scope) if (fromSession) return fromSession } return {} }, [searchParams] ) const persistFilter = useCallback( ( scope, filterState, { saveFilterInSession = false, saveFilterInUrl = false } = {} ) => { const active = getActiveFilterValues(filterState) if (saveFilterInSession) { writeFilterToSession(scope, active) } if (saveFilterInUrl) { const next = new URLSearchParams(searchParams) if (Object.keys(active).length > 0) { next.set(FILTER_URL_PARAM, JSON.stringify(active)) } else { next.delete(FILTER_URL_PARAM) } setSearchParams(next, { replace: true }) } }, [searchParams, setSearchParams] ) const persistSort = useCallback( ( scope, sorter, { saveSortInSession = false, saveSortInUrl = false } = {} ) => { const nextSorter = normalizeSorter(sorter) if (saveSortInSession) { writeSortToSession(scope, nextSorter) } if (saveSortInUrl) { const next = new URLSearchParams(searchParams) if (nextSorter.field && nextSorter.order) { next.set(SORT_URL_PARAM, JSON.stringify(nextSorter)) } else { next.delete(SORT_URL_PARAM) } setSearchParams(next, { replace: true }) } }, [searchParams, setSearchParams] ) const persistTableState = useCallback( ( scope, filterState, sorter, { saveFilterInSession = false, saveFilterInUrl = false, saveSortInSession = false, saveSortInUrl = false } = {} ) => { const active = getActiveFilterValues(filterState) const nextSorter = normalizeSorter(sorter) if (saveFilterInSession) { writeFilterToSession(scope, active) } if (saveSortInSession) { writeSortToSession(scope, nextSorter) } if (saveFilterInUrl || saveSortInUrl) { const next = new URLSearchParams(searchParams) if (saveFilterInUrl) { if (Object.keys(active).length > 0) { next.set(FILTER_URL_PARAM, JSON.stringify(active)) } else { next.delete(FILTER_URL_PARAM) } } if (saveSortInUrl) { if (nextSorter.field && nextSorter.order) { next.set(SORT_URL_PARAM, JSON.stringify(nextSorter)) } else { next.delete(SORT_URL_PARAM) } } setSearchParams(next, { replace: true }) } }, [searchParams, setSearchParams] ) const setPageFilter = useCallback((path, filter) => { const active = getActiveFilterValues(filter) setPageFilters((prev) => { const hasActive = Object.keys(active).length > 0 if (!hasActive) { if (!(path in prev)) return prev const next = { ...prev } delete next[path] return next } return { ...prev, [path]: active } }) }, []) const setPageSorter = useCallback((path, sorter) => { const hasSort = sorter?.field && sorter?.order setPageSorters((prev) => { if (!hasSort) { if (!(path in prev)) return prev const next = { ...prev } delete next[path] return next } return { ...prev, [path]: { field: sorter.field, order: sorter.order } } }) }, []) const hasPageFilter = useCallback( (path) => Object.keys(pageFilters[path] || {}).length > 0, [pageFilters] ) const hasStoredFilter = useCallback((scope) => hasStoredSessionFilter(scope), []) const value = useMemo( () => ({ getPersistedFilter, getPersistedSorter, persistFilter, persistSort, persistTableState, setPageFilter, setPageSorter, hasPageFilter, hasStoredFilter, pageFilters, pageSorters }), [ getPersistedFilter, getPersistedSorter, persistFilter, persistSort, persistTableState, setPageFilter, setPageSorter, hasPageFilter, hasStoredFilter, pageFilters, pageSorters ] ) return ( {children} ) } // eslint-disable-next-line react-refresh/only-export-components export const useTableState = () => { const context = useContext(TableStateContext) if (!context) { throw new Error('useTableState must be used within a TableStateProvider') } return context } // eslint-disable-next-line react-refresh/only-export-components export const useTableStatePersistence = ({ scope, pagePath, saveFilterInSession = false, saveFilterInUrl = false, useFilterInSession = false, useFilterInUrl = false, saveSortInSession = false, saveSortInUrl = false, useSortInSession = false, useSortInUrl = false }) => { const { getPersistedFilter: getPersistedFilterCtx, getPersistedSorter: getPersistedSorterCtx, persistFilter: persistFilterCtx, persistSort: persistSortCtx, persistTableState: persistTableStateCtx, setPageFilter, setPageSorter } = useTableState() const readOptions = useMemo( () => ({ useFilterInUrl, useFilterInSession, useSortInUrl, useSortInSession }), [useFilterInUrl, useFilterInSession, useSortInUrl, useSortInSession] ) const saveOptions = useMemo( () => ({ saveFilterInSession, saveFilterInUrl, saveSortInSession, saveSortInUrl }), [saveFilterInSession, saveFilterInUrl, saveSortInSession, saveSortInUrl] ) const getPersistedFilter = useCallback( () => getPersistedFilterCtx(scope, readOptions), [getPersistedFilterCtx, scope, readOptions] ) const getPersistedSorter = useCallback( () => getPersistedSorterCtx(scope, readOptions), [getPersistedSorterCtx, scope, readOptions] ) const persistFilter = useCallback( (filterState) => persistFilterCtx(scope, filterState, saveOptions), [persistFilterCtx, scope, saveOptions] ) const persistSort = useCallback( (sorter) => persistSortCtx(scope, sorter, saveOptions), [persistSortCtx, scope, saveOptions] ) const persistTableState = useCallback( (filterState, sorter) => persistTableStateCtx(scope, filterState, sorter, saveOptions), [persistTableStateCtx, scope, saveOptions] ) const registerPageFilter = useCallback( (filterState) => { if (pagePath) { setPageFilter(pagePath, filterState) } }, [pagePath, setPageFilter] ) const registerPageSorter = useCallback( (sorter) => { if (pagePath) { setPageSorter(pagePath, sorter) } }, [pagePath, setPageSorter] ) return { getPersistedFilter, getPersistedSorter, persistFilter, persistSort, persistTableState, registerPageFilter, registerPageSorter, getActiveFilterValues } } TableStateProvider.propTypes = { children: PropTypes.node.isRequired } export default TableStateContext