- Introduced NavigationTabIdContext to improve tab identification and management within the Dashboard. - Updated TableStateProvider to utilize the new context for better handling of tab-specific state, particularly in Electron environments. - Enhanced ObjectListViewProvider to integrate persisted view handling based on the new context, improving user experience across different navigation scenarios. - Refactored various components to streamline state management and improve overall performance.
881 lines
25 KiB
JavaScript
881 lines
25 KiB
JavaScript
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState
|
|
} from 'react'
|
|
import { useSearchParams } from 'react-router-dom'
|
|
import PropTypes from 'prop-types'
|
|
import {
|
|
NavigationTabIdContext,
|
|
useNavigationTabs
|
|
} from './NavigationTabsContext'
|
|
|
|
const TableStateContext = createContext(null)
|
|
|
|
// eslint-disable-next-line react-refresh/only-export-components
|
|
export { TableStateContext }
|
|
|
|
export const FILTER_URL_PARAM = 'filter'
|
|
export const SORT_URL_PARAM = 'sort'
|
|
export const VIEW_URL_PARAM = 'viewId'
|
|
|
|
const getSessionFilterKey = (scope) => `tableState:${scope}:filter`
|
|
const getSessionSortKey = (scope) => `tableState:${scope}:sort`
|
|
const getSessionListFilterKey = (scope) => `tableState:${scope}:listFilter`
|
|
const getSessionListSortKey = (scope) => `tableState:${scope}:listSort`
|
|
const getSessionListViewKey = (scope) => `tableState:${scope}:listView`
|
|
|
|
// 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 readViewFromUrl = (searchParams) => searchParams.get(VIEW_URL_PARAM) || null
|
|
|
|
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 readListFilterFromSession = (scope) => {
|
|
try {
|
|
return parseJsonParam(sessionStorage.getItem(getSessionListFilterKey(scope)))
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
const readListSortFromSession = (scope) => {
|
|
try {
|
|
const parsed = parseJsonParam(sessionStorage.getItem(getSessionListSortKey(scope)))
|
|
if (!parsed?.field || !parsed?.order) return null
|
|
return { field: parsed.field, order: parsed.order }
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
const writeListFilterToSession = (scope, filter) => {
|
|
const key = getSessionListFilterKey(scope)
|
|
if (Object.keys(filter).length > 0) {
|
|
sessionStorage.setItem(key, JSON.stringify(filter))
|
|
} else {
|
|
sessionStorage.removeItem(key)
|
|
}
|
|
}
|
|
|
|
const writeListSortToSession = (scope, sorter) => {
|
|
const key = getSessionListSortKey(scope)
|
|
if (sorter?.field && sorter?.order) {
|
|
sessionStorage.setItem(key, JSON.stringify(sorter))
|
|
} else {
|
|
sessionStorage.removeItem(key)
|
|
}
|
|
}
|
|
|
|
const readListViewFromSession = (scope) => {
|
|
try {
|
|
const parsed = parseJsonParam(sessionStorage.getItem(getSessionListViewKey(scope)))
|
|
if (!parsed?.id) return null
|
|
return {
|
|
id: String(parsed.id),
|
|
name: parsed.name || null
|
|
}
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
const writeListViewToSession = (scope, view) => {
|
|
const key = getSessionListViewKey(scope)
|
|
if (view?.id) {
|
|
sessionStorage.setItem(
|
|
key,
|
|
JSON.stringify({ id: String(view.id), name: view.name || null })
|
|
)
|
|
} 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 tabId = useContext(NavigationTabIdContext)
|
|
const { isElectron, getTabListState, setTabListState, activeTab } =
|
|
useNavigationTabs()
|
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
const [pageFilters, setPageFilters] = useState({})
|
|
const [pageSorters, setPageSorters] = useState({})
|
|
// Effective list filters (view filter + user filters). Survives navigating
|
|
// from the list page to an info page so breadcrumb/neighbors keep working.
|
|
const [objectListFilters, setObjectListFilters] = useState({})
|
|
const [objectListSorters, setObjectListSorters] = useState({})
|
|
// Active list view (id + name) per object type — same survival pattern as filters.
|
|
const [objectListViews, setObjectListViews] = useState({})
|
|
const searchParamsRef = useRef(searchParams)
|
|
// Tracks an in-flight optimistic URL write so a render with stale router
|
|
// searchParams cannot clobber searchParamsRef and resurrect deleted params
|
|
// (e.g. viewId briefly cleared on All, then written back by persistTableState).
|
|
const pendingSearchParamsRef = useRef(null)
|
|
const previousSearchParamsRef = useRef(searchParams.toString())
|
|
|
|
useEffect(() => {
|
|
const current = searchParams.toString()
|
|
if (pendingSearchParamsRef.current != null) {
|
|
if (current === pendingSearchParamsRef.current) {
|
|
pendingSearchParamsRef.current = null
|
|
} else if (current === previousSearchParamsRef.current) {
|
|
// Stale snapshot from before our write — keep the optimistic ref.
|
|
return
|
|
} else {
|
|
// External navigation while a write was pending — accept it.
|
|
pendingSearchParamsRef.current = null
|
|
}
|
|
}
|
|
previousSearchParamsRef.current = current
|
|
searchParamsRef.current = searchParams
|
|
}, [searchParams])
|
|
|
|
// Keep sequential URL writes in the same tick from clobbering each other
|
|
// (e.g. persistView then persistTableState both calling setSearchParams).
|
|
const updateSearchParams = useCallback(
|
|
(mutator) => {
|
|
const previous = searchParamsRef.current.toString()
|
|
const next = new URLSearchParams(searchParamsRef.current)
|
|
mutator(next)
|
|
previousSearchParamsRef.current = previous
|
|
searchParamsRef.current = next
|
|
pendingSearchParamsRef.current = next.toString()
|
|
setSearchParams(next, { replace: true })
|
|
},
|
|
[setSearchParams]
|
|
)
|
|
|
|
const resolveScopedTabId = useCallback(
|
|
() => tabId || (isElectron ? activeTab?.id : null),
|
|
[activeTab?.id, isElectron, tabId]
|
|
)
|
|
|
|
const getPersistedFilter = useCallback(
|
|
(scope, { useFilterInUrl = false, useFilterInSession = false } = {}) => {
|
|
const scopedTabId = resolveScopedTabId()
|
|
if (isElectron && scopedTabId) {
|
|
return getTabListState(scopedTabId, scope).filter || {}
|
|
}
|
|
if (useFilterInUrl) {
|
|
const fromUrl = readFilterFromUrl(searchParams)
|
|
if (fromUrl) return fromUrl
|
|
}
|
|
if (useFilterInSession) {
|
|
const fromSession = readFilterFromSession(scope)
|
|
if (fromSession) return fromSession
|
|
}
|
|
return {}
|
|
},
|
|
[getTabListState, isElectron, resolveScopedTabId, searchParams]
|
|
)
|
|
|
|
const getPersistedSorter = useCallback(
|
|
(scope, { useSortInUrl = false, useSortInSession = false } = {}) => {
|
|
const scopedTabId = resolveScopedTabId()
|
|
if (isElectron && scopedTabId) {
|
|
return getTabListState(scopedTabId, scope).sort || {}
|
|
}
|
|
if (useSortInUrl) {
|
|
const fromUrl = readSortFromUrl(searchParams)
|
|
if (fromUrl) return fromUrl
|
|
}
|
|
if (useSortInSession) {
|
|
const fromSession = readSortFromSession(scope)
|
|
if (fromSession) return fromSession
|
|
}
|
|
return {}
|
|
},
|
|
[getTabListState, isElectron, resolveScopedTabId, searchParams]
|
|
)
|
|
|
|
const getViewFromUrl = useCallback(
|
|
() => readViewFromUrl(searchParams),
|
|
[searchParams]
|
|
)
|
|
|
|
const getPersistedView = useCallback(
|
|
(scope) => {
|
|
const scopedTabId = resolveScopedTabId()
|
|
if (isElectron && scopedTabId) {
|
|
const entry = getTabListState(scopedTabId, scope)
|
|
return entry.viewId
|
|
? { id: String(entry.viewId), name: entry.viewName || null }
|
|
: null
|
|
}
|
|
const fromUrl = readViewFromUrl(searchParams)
|
|
if (fromUrl) return { id: String(fromUrl), name: null }
|
|
return readListViewFromSession(scope)
|
|
},
|
|
[getTabListState, isElectron, resolveScopedTabId, searchParams]
|
|
)
|
|
|
|
const persistView = useCallback(
|
|
(
|
|
viewId,
|
|
{ clearFilterSort = Boolean(viewId), scope, name } = {}
|
|
) => {
|
|
const scopedTabId = resolveScopedTabId()
|
|
if (isElectron && scopedTabId && scope) {
|
|
setTabListState(scopedTabId, scope, {
|
|
viewId: viewId ? String(viewId) : null,
|
|
viewName: viewId ? name || null : null
|
|
})
|
|
return
|
|
}
|
|
|
|
updateSearchParams((next) => {
|
|
// Drop legacy `view` param that previously stored `_reference`
|
|
next.delete('view')
|
|
if (viewId) {
|
|
next.set(VIEW_URL_PARAM, String(viewId))
|
|
} else {
|
|
next.delete(VIEW_URL_PARAM)
|
|
}
|
|
// View filter/sort live on the view object — never in the URL
|
|
if (clearFilterSort) {
|
|
next.delete(FILTER_URL_PARAM)
|
|
next.delete(SORT_URL_PARAM)
|
|
}
|
|
})
|
|
},
|
|
[isElectron, resolveScopedTabId, setTabListState, updateSearchParams]
|
|
)
|
|
|
|
const persistFilter = useCallback(
|
|
(
|
|
scope,
|
|
filterState,
|
|
{ saveFilterInSession = false, saveFilterInUrl = false } = {}
|
|
) => {
|
|
const active = getActiveFilterValues(filterState)
|
|
const scopedTabId = resolveScopedTabId()
|
|
if (isElectron && scopedTabId) {
|
|
if (saveFilterInSession || saveFilterInUrl) {
|
|
setTabListState(scopedTabId, scope, { filter: active })
|
|
}
|
|
return
|
|
}
|
|
if (saveFilterInSession) {
|
|
writeFilterToSession(scope, active)
|
|
}
|
|
if (saveFilterInUrl) {
|
|
updateSearchParams((next) => {
|
|
if (Object.keys(active).length > 0) {
|
|
next.set(FILTER_URL_PARAM, JSON.stringify(active))
|
|
} else {
|
|
next.delete(FILTER_URL_PARAM)
|
|
}
|
|
})
|
|
}
|
|
},
|
|
[isElectron, resolveScopedTabId, setTabListState, updateSearchParams]
|
|
)
|
|
|
|
const persistSort = useCallback(
|
|
(
|
|
scope,
|
|
sorter,
|
|
{ saveSortInSession = false, saveSortInUrl = false } = {}
|
|
) => {
|
|
const nextSorter = normalizeSorter(sorter)
|
|
const scopedTabId = resolveScopedTabId()
|
|
if (isElectron && scopedTabId) {
|
|
if (saveSortInSession || saveSortInUrl) {
|
|
setTabListState(scopedTabId, scope, { sort: nextSorter })
|
|
}
|
|
return
|
|
}
|
|
if (saveSortInSession) {
|
|
writeSortToSession(scope, nextSorter)
|
|
}
|
|
if (saveSortInUrl) {
|
|
updateSearchParams((next) => {
|
|
if (nextSorter.field && nextSorter.order) {
|
|
next.set(SORT_URL_PARAM, JSON.stringify(nextSorter))
|
|
} else {
|
|
next.delete(SORT_URL_PARAM)
|
|
}
|
|
})
|
|
}
|
|
},
|
|
[isElectron, resolveScopedTabId, setTabListState, updateSearchParams]
|
|
)
|
|
|
|
const persistTableState = useCallback(
|
|
(
|
|
scope,
|
|
filterState,
|
|
sorter,
|
|
{
|
|
saveFilterInSession = false,
|
|
saveFilterInUrl = false,
|
|
saveSortInSession = false,
|
|
saveSortInUrl = false
|
|
} = {}
|
|
) => {
|
|
const active = getActiveFilterValues(filterState)
|
|
const nextSorter = normalizeSorter(sorter)
|
|
const scopedTabId = resolveScopedTabId()
|
|
|
|
if (isElectron && scopedTabId) {
|
|
const patch = {}
|
|
if (saveFilterInSession || saveFilterInUrl) {
|
|
patch.filter = active
|
|
}
|
|
if (saveSortInSession || saveSortInUrl) {
|
|
patch.sort = nextSorter
|
|
}
|
|
if (Object.keys(patch).length > 0) {
|
|
setTabListState(scopedTabId, scope, patch)
|
|
}
|
|
return
|
|
}
|
|
|
|
if (saveFilterInSession) {
|
|
writeFilterToSession(scope, active)
|
|
}
|
|
if (saveSortInSession) {
|
|
writeSortToSession(scope, nextSorter)
|
|
}
|
|
|
|
if (saveFilterInUrl || saveSortInUrl) {
|
|
updateSearchParams((next) => {
|
|
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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
},
|
|
[isElectron, resolveScopedTabId, setTabListState, updateSearchParams]
|
|
)
|
|
|
|
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 setObjectListFilter = useCallback((objectType, filter) => {
|
|
if (!objectType) return
|
|
const active = getActiveFilterValues(filter)
|
|
const scopedTabId = resolveScopedTabId()
|
|
if (isElectron && scopedTabId) {
|
|
setTabListState(scopedTabId, objectType, { listFilter: active })
|
|
} else {
|
|
writeListFilterToSession(objectType, active)
|
|
}
|
|
setObjectListFilters((prev) => {
|
|
const hasActive = Object.keys(active).length > 0
|
|
if (!hasActive) {
|
|
if (!(objectType in prev)) return prev
|
|
const next = { ...prev }
|
|
delete next[objectType]
|
|
return next
|
|
}
|
|
return { ...prev, [objectType]: active }
|
|
})
|
|
}, [isElectron, resolveScopedTabId, setTabListState])
|
|
|
|
const setObjectListSorter = useCallback((objectType, sorter) => {
|
|
if (!objectType) return
|
|
const nextSorter = normalizeSorter(sorter)
|
|
const scopedTabId = resolveScopedTabId()
|
|
if (isElectron && scopedTabId) {
|
|
setTabListState(scopedTabId, objectType, { listSort: nextSorter })
|
|
} else {
|
|
writeListSortToSession(objectType, nextSorter)
|
|
}
|
|
setObjectListSorters((prev) => {
|
|
const hasSort = nextSorter?.field && nextSorter?.order
|
|
if (!hasSort) {
|
|
if (!(objectType in prev)) return prev
|
|
const next = { ...prev }
|
|
delete next[objectType]
|
|
return next
|
|
}
|
|
return { ...prev, [objectType]: nextSorter }
|
|
})
|
|
}, [isElectron, resolveScopedTabId, setTabListState])
|
|
|
|
const setObjectListView = useCallback((objectType, view) => {
|
|
if (!objectType) return
|
|
const nextView =
|
|
view?.id != null
|
|
? { id: String(view.id), name: view.name || null }
|
|
: null
|
|
const scopedTabId = resolveScopedTabId()
|
|
if (isElectron && scopedTabId) {
|
|
setTabListState(scopedTabId, objectType, {
|
|
viewId: nextView?.id || null,
|
|
viewName: nextView?.name || null
|
|
})
|
|
} else {
|
|
writeListViewToSession(objectType, nextView)
|
|
}
|
|
setObjectListViews((prev) => {
|
|
if (!nextView) {
|
|
if (!(objectType in prev)) return prev
|
|
const next = { ...prev }
|
|
delete next[objectType]
|
|
return next
|
|
}
|
|
const existing = prev[objectType]
|
|
if (
|
|
existing &&
|
|
existing.id === nextView.id &&
|
|
existing.name === nextView.name
|
|
) {
|
|
return prev
|
|
}
|
|
return { ...prev, [objectType]: nextView }
|
|
})
|
|
}, [isElectron, resolveScopedTabId, setTabListState])
|
|
|
|
const getObjectListFilter = useCallback(
|
|
(objectType) => {
|
|
if (!objectType) return {}
|
|
if (objectListFilters[objectType]) {
|
|
return objectListFilters[objectType]
|
|
}
|
|
const scopedTabId = resolveScopedTabId()
|
|
if (isElectron && scopedTabId) {
|
|
return getActiveFilterValues(
|
|
getTabListState(scopedTabId, objectType).listFilter || {}
|
|
)
|
|
}
|
|
return getActiveFilterValues(readListFilterFromSession(objectType) || {})
|
|
},
|
|
[getTabListState, isElectron, objectListFilters, resolveScopedTabId]
|
|
)
|
|
|
|
const getObjectListSorter = useCallback(
|
|
(objectType) => {
|
|
if (!objectType) return {}
|
|
if (objectListSorters[objectType]) {
|
|
return objectListSorters[objectType]
|
|
}
|
|
const scopedTabId = resolveScopedTabId()
|
|
if (isElectron && scopedTabId) {
|
|
return getTabListState(scopedTabId, objectType).listSort || {}
|
|
}
|
|
return readListSortFromSession(objectType) || {}
|
|
},
|
|
[getTabListState, isElectron, objectListSorters, resolveScopedTabId]
|
|
)
|
|
|
|
const getObjectListView = useCallback(
|
|
(objectType) => {
|
|
if (!objectType) return null
|
|
if (objectListViews[objectType]) {
|
|
return objectListViews[objectType]
|
|
}
|
|
const scopedTabId = resolveScopedTabId()
|
|
if (isElectron && scopedTabId) {
|
|
const entry = getTabListState(scopedTabId, objectType)
|
|
return entry.viewId
|
|
? { id: String(entry.viewId), name: entry.viewName || null }
|
|
: null
|
|
}
|
|
return readListViewFromSession(objectType)
|
|
},
|
|
[getTabListState, isElectron, objectListViews, resolveScopedTabId]
|
|
)
|
|
|
|
const hasPageFilter = useCallback(
|
|
(path) => Object.keys(pageFilters[path] || {}).length > 0,
|
|
[pageFilters]
|
|
)
|
|
|
|
const hasStoredFilter = useCallback((scope) => {
|
|
if (!scope) return false
|
|
const scopedTabId = resolveScopedTabId()
|
|
if (isElectron && scopedTabId) {
|
|
const entry = getTabListState(scopedTabId, scope)
|
|
if (entry.viewId) {
|
|
return Object.keys(entry.listFilter || {}).length > 0
|
|
}
|
|
return (
|
|
Object.keys(entry.filter || {}).length > 0 ||
|
|
Object.keys(entry.listFilter || {}).length > 0
|
|
)
|
|
}
|
|
// When a view is active, only report the effective list filter (which
|
|
// already accounts for the view). Don't fall back to the All-tab session
|
|
// filter — that belongs to a different context.
|
|
if (objectListViews[scope]) {
|
|
return Object.keys(objectListFilters[scope] || {}).length > 0
|
|
}
|
|
if (hasStoredSessionFilter(scope)) return true
|
|
if (Object.keys(objectListFilters[scope] || {}).length > 0) return true
|
|
return Object.keys(getActiveFilterValues(readListFilterFromSession(scope) || {})).length > 0
|
|
}, [
|
|
getTabListState,
|
|
isElectron,
|
|
objectListFilters,
|
|
objectListViews,
|
|
resolveScopedTabId
|
|
])
|
|
|
|
const value = useMemo(
|
|
() => ({
|
|
getPersistedFilter,
|
|
getPersistedSorter,
|
|
getPersistedView,
|
|
getViewFromUrl,
|
|
persistView,
|
|
persistFilter,
|
|
persistSort,
|
|
persistTableState,
|
|
setPageFilter,
|
|
setPageSorter,
|
|
setObjectListFilter,
|
|
setObjectListSorter,
|
|
setObjectListView,
|
|
getObjectListFilter,
|
|
getObjectListSorter,
|
|
getObjectListView,
|
|
hasPageFilter,
|
|
hasStoredFilter,
|
|
pageFilters,
|
|
pageSorters,
|
|
objectListFilters,
|
|
objectListSorters,
|
|
objectListViews
|
|
}),
|
|
[
|
|
getPersistedFilter,
|
|
getPersistedSorter,
|
|
getPersistedView,
|
|
getViewFromUrl,
|
|
persistView,
|
|
persistFilter,
|
|
persistSort,
|
|
persistTableState,
|
|
setPageFilter,
|
|
setPageSorter,
|
|
setObjectListFilter,
|
|
setObjectListSorter,
|
|
setObjectListView,
|
|
getObjectListFilter,
|
|
getObjectListSorter,
|
|
getObjectListView,
|
|
hasPageFilter,
|
|
hasStoredFilter,
|
|
pageFilters,
|
|
pageSorters,
|
|
objectListFilters,
|
|
objectListSorters,
|
|
objectListViews
|
|
]
|
|
)
|
|
|
|
return (
|
|
<TableStateContext.Provider value={value}>
|
|
{children}
|
|
</TableStateContext.Provider>
|
|
)
|
|
}
|
|
|
|
// 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,
|
|
viewId = null,
|
|
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,
|
|
setObjectListFilter,
|
|
setObjectListSorter
|
|
} = useTableState()
|
|
|
|
// View filter/sort belong to the view object — never read/write them via
|
|
// the shared filter/sort URL or session keys used by the "All" tab.
|
|
const isViewScope = Boolean(viewId)
|
|
|
|
const readOptions = useMemo(
|
|
() => ({
|
|
useFilterInUrl: isViewScope ? false : useFilterInUrl,
|
|
useFilterInSession: isViewScope ? false : useFilterInSession,
|
|
useSortInUrl: isViewScope ? false : useSortInUrl,
|
|
useSortInSession: isViewScope ? false : useSortInSession
|
|
}),
|
|
[
|
|
isViewScope,
|
|
useFilterInUrl,
|
|
useFilterInSession,
|
|
useSortInUrl,
|
|
useSortInSession
|
|
]
|
|
)
|
|
|
|
const saveOptions = useMemo(
|
|
() => ({
|
|
saveFilterInSession: isViewScope ? false : saveFilterInSession,
|
|
saveFilterInUrl: isViewScope ? false : saveFilterInUrl,
|
|
saveSortInSession: isViewScope ? false : saveSortInSession,
|
|
saveSortInUrl: isViewScope ? false : saveSortInUrl
|
|
}),
|
|
[
|
|
isViewScope,
|
|
saveFilterInSession,
|
|
saveFilterInUrl,
|
|
saveSortInSession,
|
|
saveSortInUrl
|
|
]
|
|
)
|
|
|
|
const allTabReadOptions = useMemo(
|
|
() => ({
|
|
useFilterInUrl,
|
|
useFilterInSession,
|
|
useSortInUrl,
|
|
useSortInSession
|
|
}),
|
|
[useFilterInUrl, useFilterInSession, useSortInUrl, useSortInSession]
|
|
)
|
|
|
|
const allTabSaveOptions = 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]
|
|
)
|
|
|
|
// Always reads the All-tab filter/sort, even while a view is still selected
|
|
// (used when switching back to All before listViewId has cleared).
|
|
const getAllTabPersistedFilter = useCallback(
|
|
() => getPersistedFilterCtx(scope, allTabReadOptions),
|
|
[getPersistedFilterCtx, scope, allTabReadOptions]
|
|
)
|
|
|
|
const getAllTabPersistedSorter = useCallback(
|
|
() => getPersistedSorterCtx(scope, allTabReadOptions),
|
|
[getPersistedSorterCtx, scope, allTabReadOptions]
|
|
)
|
|
|
|
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 persistAllTabTableState = useCallback(
|
|
(filterState, sorter) =>
|
|
persistTableStateCtx(scope, filterState, sorter, allTabSaveOptions),
|
|
[persistTableStateCtx, scope, allTabSaveOptions]
|
|
)
|
|
|
|
const registerPageFilter = useCallback(
|
|
(filterState) => {
|
|
if (pagePath) {
|
|
setPageFilter(pagePath, filterState)
|
|
}
|
|
},
|
|
[pagePath, setPageFilter]
|
|
)
|
|
|
|
const registerPageSorter = useCallback(
|
|
(sorter) => {
|
|
if (pagePath) {
|
|
setPageSorter(pagePath, sorter)
|
|
}
|
|
},
|
|
[pagePath, setPageSorter]
|
|
)
|
|
|
|
const registerObjectListFilter = useCallback(
|
|
(filterState) => {
|
|
if (scope) {
|
|
setObjectListFilter(scope, filterState)
|
|
}
|
|
},
|
|
[scope, setObjectListFilter]
|
|
)
|
|
|
|
const registerObjectListSorter = useCallback(
|
|
(sorter) => {
|
|
if (scope) {
|
|
setObjectListSorter(scope, sorter)
|
|
}
|
|
},
|
|
[scope, setObjectListSorter]
|
|
)
|
|
|
|
return {
|
|
persistenceScope: scope,
|
|
getPersistedFilter,
|
|
getPersistedSorter,
|
|
getAllTabPersistedFilter,
|
|
getAllTabPersistedSorter,
|
|
persistFilter,
|
|
persistSort,
|
|
persistTableState,
|
|
persistAllTabTableState,
|
|
registerPageFilter,
|
|
registerPageSorter,
|
|
registerObjectListFilter,
|
|
registerObjectListSorter,
|
|
getActiveFilterValues
|
|
}
|
|
}
|
|
|
|
TableStateProvider.propTypes = {
|
|
children: PropTypes.node.isRequired
|
|
}
|
|
|
|
export default TableStateContext
|