Enhance Dashboard Breadcrumb and ObjectListView Context with View Management
- Updated DashboardBreadcrumb to display a more informative name by incorporating the view name alongside the model label, improving user navigation clarity. - Enhanced ObjectListViewProvider to synchronize the active view name with the dashboard breadcrumb, ensuring consistent view representation across components. - Introduced session management for object list views in TableStateContext, allowing for persistent view states across user sessions.
This commit is contained in:
parent
9d1a032e10
commit
9b344b6a3d
@ -28,7 +28,7 @@ const mainSections = ['production', 'inventory', 'management', 'developer']
|
||||
const DashboardBreadcrumb = () => {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const { hasPageFilter, hasStoredFilter } = useTableState()
|
||||
const { hasPageFilter, hasStoredFilter, getObjectListView } = useTableState()
|
||||
const pathSnippets = location.pathname.split('/').filter((i) => i)
|
||||
|
||||
function segmentToModel(segment) {
|
||||
@ -45,7 +45,15 @@ const DashboardBreadcrumb = () => {
|
||||
const isMainSection = mainSections.includes(segment)
|
||||
const model = segmentToModel(segment)
|
||||
const modelLabelPlural = model?.labelPlural ? model.labelPlural : null
|
||||
const viewName =
|
||||
modelLabelPlural && model?.name && model.name !== 'unknown'
|
||||
? getObjectListView(model.name)?.name
|
||||
: null
|
||||
const name = breadcrumbNameMap[segment] || modelLabelPlural || segment
|
||||
const displayName =
|
||||
modelLabelPlural && viewName
|
||||
? `${modelLabelPlural} - ${viewName}`
|
||||
: name
|
||||
const showFilterIcon =
|
||||
hasPageFilter(url) ||
|
||||
(model?.name && model.name !== 'unknown' && hasStoredFilter(model.name))
|
||||
@ -53,7 +61,7 @@ const DashboardBreadcrumb = () => {
|
||||
return {
|
||||
title: (
|
||||
<span style={{ padding: '0 12px' }} key={segment}>
|
||||
{name}
|
||||
{displayName}
|
||||
</span>
|
||||
),
|
||||
key: segment
|
||||
@ -71,7 +79,7 @@ const DashboardBreadcrumb = () => {
|
||||
gap: 6
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
{displayName}
|
||||
{showFilterIcon && <FilterIcon style={{ fontSize: 12 }} />}
|
||||
</Link>
|
||||
),
|
||||
|
||||
@ -127,7 +127,7 @@ export const ObjectListViewProvider = ({
|
||||
subscribeToObjectUpdates
|
||||
} = useContext(ApiServerContext)
|
||||
const { token, authInitialized } = useContext(AuthContext)
|
||||
const { getViewFromUrl, persistView } = useTableState()
|
||||
const { getViewFromUrl, persistView, setObjectListView } = useTableState()
|
||||
|
||||
const [views, setViews] = useState(
|
||||
() => readCachedViews(objectType) || []
|
||||
@ -187,6 +187,16 @@ export const ObjectListViewProvider = ({
|
||||
viewsStateRef.current = { views, draftViews, isEditing }
|
||||
activeViewRef.current = activeView
|
||||
|
||||
// Keep the remembered view name in sync (incl. renames while editing) for
|
||||
// consumers outside this provider (e.g. the dashboard breadcrumb).
|
||||
useEffect(() => {
|
||||
if (!objectType || !activeView) return
|
||||
setObjectListView(objectType, {
|
||||
id: String(activeView._id),
|
||||
name: activeView.name || null
|
||||
})
|
||||
}, [objectType, activeView, setObjectListView])
|
||||
|
||||
const applyViewToTable = useCallback(
|
||||
(tabKey) => {
|
||||
if (!tableRef?.current?.applyViewState) return
|
||||
@ -253,12 +263,26 @@ export const ObjectListViewProvider = ({
|
||||
loadViewsRef.current = loadViews
|
||||
|
||||
const rememberActiveView = useCallback(
|
||||
(viewId) => {
|
||||
const key = viewId ? String(viewId) : null
|
||||
(view) => {
|
||||
const key =
|
||||
view == null
|
||||
? null
|
||||
: typeof view === 'string' || typeof view === 'number'
|
||||
? String(view)
|
||||
: view?._id != null
|
||||
? String(view._id)
|
||||
: null
|
||||
const name =
|
||||
key && view && typeof view === 'object' ? view.name || null : null
|
||||
|
||||
persistView(key)
|
||||
writeLastViewId(objectType, key)
|
||||
setObjectListView(
|
||||
objectType,
|
||||
key ? { id: key, name } : null
|
||||
)
|
||||
},
|
||||
[objectType, persistView]
|
||||
[objectType, persistView, setObjectListView]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@ -327,6 +351,10 @@ export const ObjectListViewProvider = ({
|
||||
const matchedId = String(matched._id)
|
||||
setActiveTabKey(matchedId)
|
||||
writeLastViewId(objectType, matchedId)
|
||||
setObjectListView(objectType, {
|
||||
id: matchedId,
|
||||
name: matched.name || null
|
||||
})
|
||||
if (!matched._isDraft) {
|
||||
// Keep viewId in the URL and strip any leftover filter/sort params
|
||||
persistView(matchedId)
|
||||
@ -354,7 +382,8 @@ export const ObjectListViewProvider = ({
|
||||
objectType,
|
||||
getViewFromUrl,
|
||||
persistView,
|
||||
rememberActiveView
|
||||
rememberActiveView,
|
||||
setObjectListView
|
||||
])
|
||||
|
||||
const selectTab = useCallback(
|
||||
@ -373,7 +402,7 @@ export const ObjectListViewProvider = ({
|
||||
)
|
||||
if (!view || view._isDraft) return
|
||||
|
||||
rememberActiveView(getObjectViewKey(view))
|
||||
rememberActiveView(view)
|
||||
},
|
||||
[activeTabKey, draftViews, isEditing, rememberActiveView, views]
|
||||
)
|
||||
@ -483,7 +512,7 @@ export const ObjectListViewProvider = ({
|
||||
|
||||
if (resolvedTabKey !== ALL_TAB_KEY && savedView && !savedView._isDraft) {
|
||||
setActiveTabKey(resolvedTabKey)
|
||||
rememberActiveView(getObjectViewKey(savedView))
|
||||
rememberActiveView(savedView)
|
||||
} else if (resolvedTabKey !== ALL_TAB_KEY && !savedView) {
|
||||
resolvedTabKey = ALL_TAB_KEY
|
||||
setActiveTabKey(ALL_TAB_KEY)
|
||||
@ -535,7 +564,7 @@ export const ObjectListViewProvider = ({
|
||||
const newKey = String(created._id)
|
||||
setActiveTabKey(newKey)
|
||||
applyViewToTable(newKey)
|
||||
rememberActiveView(getObjectViewKey(created))
|
||||
rememberActiveView(created)
|
||||
} else {
|
||||
message.error('Failed to create view')
|
||||
}
|
||||
@ -571,7 +600,7 @@ export const ObjectListViewProvider = ({
|
||||
(item) => String(item._id) === String(previousKey)
|
||||
)
|
||||
if (view && !view._isDraft) {
|
||||
rememberActiveView(getObjectViewKey(view))
|
||||
rememberActiveView(view)
|
||||
}
|
||||
}
|
||||
applyViewToTable(previousKey)
|
||||
@ -665,7 +694,7 @@ export const ObjectListViewProvider = ({
|
||||
(item) => String(item._id) === String(previousKey)
|
||||
)
|
||||
if (view && !view._isDraft) {
|
||||
rememberActiveView(getObjectViewKey(view))
|
||||
rememberActiveView(view)
|
||||
}
|
||||
}
|
||||
applyViewToTable(previousKey)
|
||||
|
||||
@ -2,6 +2,7 @@ import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
@ -19,6 +20,7 @@ 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) => {
|
||||
@ -128,6 +130,31 @@ const writeListSortToSession = (scope, sorter) => {
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
@ -143,16 +170,42 @@ export const TableStateProvider = ({ children }) => {
|
||||
// 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)
|
||||
searchParamsRef.current = 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]
|
||||
@ -362,6 +415,32 @@ export const TableStateProvider = ({ children }) => {
|
||||
})
|
||||
}, [])
|
||||
|
||||
const setObjectListView = useCallback((objectType, view) => {
|
||||
if (!objectType) return
|
||||
const nextView =
|
||||
view?.id != null
|
||||
? { id: String(view.id), name: view.name || null }
|
||||
: null
|
||||
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 }
|
||||
})
|
||||
}, [])
|
||||
|
||||
const getObjectListFilter = useCallback(
|
||||
(objectType) => {
|
||||
if (!objectType) return {}
|
||||
@ -384,6 +463,17 @@ export const TableStateProvider = ({ children }) => {
|
||||
[objectListSorters]
|
||||
)
|
||||
|
||||
const getObjectListView = useCallback(
|
||||
(objectType) => {
|
||||
if (!objectType) return null
|
||||
if (objectListViews[objectType]) {
|
||||
return objectListViews[objectType]
|
||||
}
|
||||
return readListViewFromSession(objectType)
|
||||
},
|
||||
[objectListViews]
|
||||
)
|
||||
|
||||
const hasPageFilter = useCallback(
|
||||
(path) => Object.keys(pageFilters[path] || {}).length > 0,
|
||||
[pageFilters]
|
||||
@ -409,14 +499,17 @@ export const TableStateProvider = ({ children }) => {
|
||||
setPageSorter,
|
||||
setObjectListFilter,
|
||||
setObjectListSorter,
|
||||
setObjectListView,
|
||||
getObjectListFilter,
|
||||
getObjectListSorter,
|
||||
getObjectListView,
|
||||
hasPageFilter,
|
||||
hasStoredFilter,
|
||||
pageFilters,
|
||||
pageSorters,
|
||||
objectListFilters,
|
||||
objectListSorters
|
||||
objectListSorters,
|
||||
objectListViews
|
||||
}),
|
||||
[
|
||||
getPersistedFilter,
|
||||
@ -430,14 +523,17 @@ export const TableStateProvider = ({ children }) => {
|
||||
setPageSorter,
|
||||
setObjectListFilter,
|
||||
setObjectListSorter,
|
||||
setObjectListView,
|
||||
getObjectListFilter,
|
||||
getObjectListSorter,
|
||||
getObjectListView,
|
||||
hasPageFilter,
|
||||
hasStoredFilter,
|
||||
pageFilters,
|
||||
pageSorters,
|
||||
objectListFilters,
|
||||
objectListSorters
|
||||
objectListSorters,
|
||||
objectListViews
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user