diff --git a/src/components/Dashboard/Layout.jsx b/src/components/Dashboard/Layout.jsx
index 56160aa..49a7b7b 100644
--- a/src/components/Dashboard/Layout.jsx
+++ b/src/components/Dashboard/Layout.jsx
@@ -12,6 +12,7 @@ import DashboardBreadcrumb from './common/DashboardBreadcrumb'
import DeveloperSidebar from './Developer/DeveloperSidebar'
import { useThemeContext } from './context/ThemeContext'
import { MessageProvider } from './context/MessageContext'
+import { TableStateProvider } from './context/TableStateContext'
const { Content } = Layout
@@ -28,40 +29,42 @@ const DashboardLayout = ({ children }) => {
return (
-
-
-
- {isProduction ? (
-
- ) : isInventory ? (
-
- ) : isFinance ? (
-
- ) : isSales ? (
-
- ) : isManagement ? (
-
- ) : isDeveloper ? (
-
- ) : (
- // Default to production sidebar
- )}
-
-
-
-
-
-
+
+
+
+
+ {isProduction ? (
+
+ ) : isInventory ? (
+
+ ) : isFinance ? (
+
+ ) : isSales ? (
+
+ ) : isManagement ? (
+
+ ) : isDeveloper ? (
+
+ ) : (
+ // Default to production sidebar
+ )}
+
+
+
+
+
+
- {children}
-
-
+ {children}
+
+
+
-
+
)
}
diff --git a/src/components/Dashboard/common/DashboardBreadcrumb.jsx b/src/components/Dashboard/common/DashboardBreadcrumb.jsx
index b042a15..3575588 100644
--- a/src/components/Dashboard/common/DashboardBreadcrumb.jsx
+++ b/src/components/Dashboard/common/DashboardBreadcrumb.jsx
@@ -3,7 +3,9 @@ import { Breadcrumb, Button, Flex, Space } from 'antd'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import ArrowLeftIcon from '../../Icons/ArrowLeftIcon'
import ArrowRightIcon from '../../Icons/ArrowRightIcon'
+import FilterIcon from '../../Icons/FilterIcon'
import { getModelByName } from '../../../database/ObjectModels'
+import { useTableState } from '../context/TableStateContext'
const breadcrumbNameMap = {
production: 'Production',
@@ -26,6 +28,7 @@ const mainSections = ['production', 'inventory', 'management', 'developer']
const DashboardBreadcrumb = () => {
const location = useLocation()
const navigate = useNavigate()
+ const { hasPageFilter, hasStoredFilter } = useTableState()
const pathSnippets = location.pathname.split('/').filter((i) => i)
function segmentToModel(segment) {
@@ -48,6 +51,9 @@ const DashboardBreadcrumb = () => {
const model = segmentToModel(segment)
const modelLabelPlural = model?.label ? `${model.label}s` : null
const name = breadcrumbNameMap[segment] || modelLabelPlural || segment
+ const showFilterIcon =
+ hasPageFilter(url) ||
+ (model?.name && model.name !== 'unknown' && hasStoredFilter(model.name))
if (isMainSection) {
return {
title: (
@@ -61,8 +67,19 @@ const DashboardBreadcrumb = () => {
return {
title: (
-
+
{name}
+ {showFilterIcon && (
+
+ )}
),
key: url
diff --git a/src/components/Dashboard/common/ObjectTable.jsx b/src/components/Dashboard/common/ObjectTable.jsx
index bcf2553..8647ae8 100644
--- a/src/components/Dashboard/common/ObjectTable.jsx
+++ b/src/components/Dashboard/common/ObjectTable.jsx
@@ -39,13 +39,14 @@ import ObjectCard from './ObjectCard'
import FilterSidebar from './FilterSidebar'
import XMarkIcon from '../../Icons/XMarkIcon'
import CheckIcon from '../../Icons/CheckIcon'
-import { useNavigate } from 'react-router-dom'
+import { useNavigate, useLocation } from 'react-router-dom'
import QuestionCircleIcon from '../../Icons/QuestionCircleIcon'
import { AuthContext } from '../context/AuthContext'
import { ElectronContext } from '../context/ElectronContext'
import ActionsIcon from '../../Icons/ActionsIcon'
import FilterIcon from '../../Icons/FilterIcon'
import ScrollBox from './ScrollBox'
+import { useTableStatePersistence } from '../context/TableStateContext'
const logger = loglevel.getLogger('DasboardTable')
logger.setLevel(config.logLevel)
@@ -117,6 +118,14 @@ const ObjectTable = forwardRef(
showFilterSidebar = false,
expandHeight = false,
showActions = true,
+ saveFilterInSession = false,
+ saveFilterInUrl = false,
+ useFilterInSession = false,
+ useFilterInUrl = false,
+ saveSortInSession = false,
+ saveSortInUrl = false,
+ useSortInSession = false,
+ useSortInUrl = false,
onRowAction
},
ref
@@ -139,6 +148,26 @@ const ObjectTable = forwardRef(
} = useContext(ApiServerContext)
const isMobile = useMediaQuery({ maxWidth: 768 })
const navigate = useNavigate()
+ const location = useLocation()
+ const {
+ getPersistedFilter,
+ getPersistedSorter,
+ persistFilter,
+ persistTableState,
+ registerPageFilter,
+ getActiveFilterValues
+ } = useTableStatePersistence({
+ scope: type,
+ pagePath: location.pathname,
+ saveFilterInSession,
+ saveFilterInUrl,
+ useFilterInSession,
+ useFilterInUrl,
+ saveSortInSession,
+ saveSortInUrl,
+ useSortInSession,
+ useSortInUrl
+ })
var adjustedScrollHeight = scrollHeight
if (isMobile) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 298px)'
@@ -160,6 +189,7 @@ const ObjectTable = forwardRef(
const activeFilterRef = useRef({})
const tableSorterRef = useRef({})
const [sidebarFilter, setSidebarFilter] = useState({})
+ const [tableSorter, setTableSorter] = useState({})
const [initialized, setInitialized] = useState(false)
// Table state
@@ -781,8 +811,24 @@ const ObjectTable = forwardRef(
)
const loadInitialPage = useCallback(async () => {
- loadPage(initialPage)
- }, [initialPage, loadPage])
+ const filter = getPersistedFilter()
+ const sorter = getPersistedSorter()
+ const activeFilter = getActiveFilterValues(filter)
+
+ if (Object.keys(filter).length > 0) {
+ setSidebarFilter(filter)
+ }
+ if (sorter.field) {
+ tableSorterRef.current = sorter
+ setTableSorter(sorter)
+ }
+
+ loadPage(
+ initialPage,
+ activeFilter,
+ sorter.field ? sorter : null
+ )
+ }, [getPersistedFilter, getPersistedSorter, initialPage, loadPage])
useImperativeHandle(ref, () => ({
reload,
@@ -823,6 +869,7 @@ const ObjectTable = forwardRef(
activeFilterRef.current = {}
tableSorterRef.current = {}
setSidebarFilter({})
+ setTableSorter({})
setInitialized(false)
setLoading(true)
setLazyLoading(false)
@@ -830,6 +877,11 @@ const ObjectTable = forwardRef(
}
}, [type, masterFilter])
+ useEffect(() => {
+ registerPageFilter(sidebarFilter)
+ return () => registerPageFilter({})
+ }, [sidebarFilter, registerPageFilter])
+
const getFilterDropdown = ({
setSelectedKeys,
selectedKeys,
@@ -866,14 +918,6 @@ const ObjectTable = forwardRef(
)
}
- const getActiveFilter = useCallback((filterState) => {
- const active = {}
- Object.entries(filterState).forEach(([k, v]) => {
- if (v !== '' && v !== undefined) active[k] = v
- })
- return active
- }, [])
-
const handleTableChange = (pagination, filters, sorter) => {
const next = { ...sidebarFilter }
@@ -885,23 +929,28 @@ const ObjectTable = forwardRef(
}
})
- setSidebarFilter(next)
- setPages([])
- setLoading(true)
- loadPage(initialPage, getActiveFilter(next), {
+ const nextSorter = {
field: sorter.columnKey,
order: sorter.order
- })
+ }
+
+ setSidebarFilter(next)
+ setTableSorter(nextSorter)
+ persistTableState(next, nextSorter)
+ setPages([])
+ setLoading(true)
+ loadPage(initialPage, getActiveFilterValues(next), nextSorter)
}
const handleSidebarFilterChange = useCallback(
(newSidebarFilter) => {
setSidebarFilter(newSidebarFilter)
+ persistFilter(newSidebarFilter)
setPages([])
setLoading(true)
- loadPage(initialPage, getActiveFilter(newSidebarFilter))
+ loadPage(initialPage, getActiveFilterValues(newSidebarFilter))
},
- [initialPage, loadPage, getActiveFilter]
+ [initialPage, loadPage, persistFilter]
)
const modelProperties = getModelProperties(type)
@@ -988,6 +1037,8 @@ const ObjectTable = forwardRef(
const columnConfig = {
sorter: isSortable ? { multiple: 1 } : undefined,
+ sortOrder:
+ tableSorter?.field === prop.name ? tableSorter.order : null,
title: prop.label,
width: prop.columnWidth || width,
fixed: isMobile ? undefined : fixed,
@@ -1306,6 +1357,14 @@ ObjectTable.propTypes = {
showFilterSidebar: PropTypes.bool,
expandHeight: PropTypes.bool,
showActions: PropTypes.bool,
+ saveFilterInSession: PropTypes.bool,
+ saveFilterInUrl: PropTypes.bool,
+ useFilterInSession: PropTypes.bool,
+ useFilterInUrl: PropTypes.bool,
+ saveSortInSession: PropTypes.bool,
+ saveSortInUrl: PropTypes.bool,
+ useSortInSession: PropTypes.bool,
+ useSortInUrl: PropTypes.bool,
onRowAction: PropTypes.func
}
diff --git a/src/components/Dashboard/context/TableStateContext.jsx b/src/components/Dashboard/context/TableStateContext.jsx
new file mode 100644
index 0000000..59981db
--- /dev/null
+++ b/src/components/Dashboard/context/TableStateContext.jsx
@@ -0,0 +1,365 @@
+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`
+
+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
+ }
+}
+
+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 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 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,
+ hasPageFilter,
+ hasStoredFilter,
+ pageFilters
+ }),
+ [
+ getPersistedFilter,
+ getPersistedSorter,
+ persistFilter,
+ persistSort,
+ persistTableState,
+ setPageFilter,
+ hasPageFilter,
+ hasStoredFilter,
+ pageFilters
+ ]
+ )
+
+ return (
+
+ {children}
+
+ )
+}
+
+export const useTableState = () => {
+ const context = useContext(TableStateContext)
+ if (!context) {
+ throw new Error('useTableState must be used within a TableStateProvider')
+ }
+ return context
+}
+
+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
+ } = 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]
+ )
+
+ return {
+ getPersistedFilter,
+ getPersistedSorter,
+ persistFilter,
+ persistSort,
+ persistTableState,
+ registerPageFilter,
+ getActiveFilterValues
+ }
+}
+
+TableStateProvider.propTypes = {
+ children: PropTypes.node.isRequired
+}
+
+export default TableStateContext