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 DashboardBreadcrumb = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { hasPageFilter, hasStoredFilter } = useTableState()
|
const { hasPageFilter, hasStoredFilter, getObjectListView } = useTableState()
|
||||||
const pathSnippets = location.pathname.split('/').filter((i) => i)
|
const pathSnippets = location.pathname.split('/').filter((i) => i)
|
||||||
|
|
||||||
function segmentToModel(segment) {
|
function segmentToModel(segment) {
|
||||||
@ -45,7 +45,15 @@ const DashboardBreadcrumb = () => {
|
|||||||
const isMainSection = mainSections.includes(segment)
|
const isMainSection = mainSections.includes(segment)
|
||||||
const model = segmentToModel(segment)
|
const model = segmentToModel(segment)
|
||||||
const modelLabelPlural = model?.labelPlural ? model.labelPlural : null
|
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 name = breadcrumbNameMap[segment] || modelLabelPlural || segment
|
||||||
|
const displayName =
|
||||||
|
modelLabelPlural && viewName
|
||||||
|
? `${modelLabelPlural} - ${viewName}`
|
||||||
|
: name
|
||||||
const showFilterIcon =
|
const showFilterIcon =
|
||||||
hasPageFilter(url) ||
|
hasPageFilter(url) ||
|
||||||
(model?.name && model.name !== 'unknown' && hasStoredFilter(model.name))
|
(model?.name && model.name !== 'unknown' && hasStoredFilter(model.name))
|
||||||
@ -53,7 +61,7 @@ const DashboardBreadcrumb = () => {
|
|||||||
return {
|
return {
|
||||||
title: (
|
title: (
|
||||||
<span style={{ padding: '0 12px' }} key={segment}>
|
<span style={{ padding: '0 12px' }} key={segment}>
|
||||||
{name}
|
{displayName}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
key: segment
|
key: segment
|
||||||
@ -71,7 +79,7 @@ const DashboardBreadcrumb = () => {
|
|||||||
gap: 6
|
gap: 6
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{name}
|
{displayName}
|
||||||
{showFilterIcon && <FilterIcon style={{ fontSize: 12 }} />}
|
{showFilterIcon && <FilterIcon style={{ fontSize: 12 }} />}
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
|
|||||||
@ -127,7 +127,7 @@ export const ObjectListViewProvider = ({
|
|||||||
subscribeToObjectUpdates
|
subscribeToObjectUpdates
|
||||||
} = useContext(ApiServerContext)
|
} = useContext(ApiServerContext)
|
||||||
const { token, authInitialized } = useContext(AuthContext)
|
const { token, authInitialized } = useContext(AuthContext)
|
||||||
const { getViewFromUrl, persistView } = useTableState()
|
const { getViewFromUrl, persistView, setObjectListView } = useTableState()
|
||||||
|
|
||||||
const [views, setViews] = useState(
|
const [views, setViews] = useState(
|
||||||
() => readCachedViews(objectType) || []
|
() => readCachedViews(objectType) || []
|
||||||
@ -187,6 +187,16 @@ export const ObjectListViewProvider = ({
|
|||||||
viewsStateRef.current = { views, draftViews, isEditing }
|
viewsStateRef.current = { views, draftViews, isEditing }
|
||||||
activeViewRef.current = activeView
|
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(
|
const applyViewToTable = useCallback(
|
||||||
(tabKey) => {
|
(tabKey) => {
|
||||||
if (!tableRef?.current?.applyViewState) return
|
if (!tableRef?.current?.applyViewState) return
|
||||||
@ -253,12 +263,26 @@ export const ObjectListViewProvider = ({
|
|||||||
loadViewsRef.current = loadViews
|
loadViewsRef.current = loadViews
|
||||||
|
|
||||||
const rememberActiveView = useCallback(
|
const rememberActiveView = useCallback(
|
||||||
(viewId) => {
|
(view) => {
|
||||||
const key = viewId ? String(viewId) : null
|
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)
|
persistView(key)
|
||||||
writeLastViewId(objectType, key)
|
writeLastViewId(objectType, key)
|
||||||
|
setObjectListView(
|
||||||
|
objectType,
|
||||||
|
key ? { id: key, name } : null
|
||||||
|
)
|
||||||
},
|
},
|
||||||
[objectType, persistView]
|
[objectType, persistView, setObjectListView]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -327,6 +351,10 @@ export const ObjectListViewProvider = ({
|
|||||||
const matchedId = String(matched._id)
|
const matchedId = String(matched._id)
|
||||||
setActiveTabKey(matchedId)
|
setActiveTabKey(matchedId)
|
||||||
writeLastViewId(objectType, matchedId)
|
writeLastViewId(objectType, matchedId)
|
||||||
|
setObjectListView(objectType, {
|
||||||
|
id: matchedId,
|
||||||
|
name: matched.name || null
|
||||||
|
})
|
||||||
if (!matched._isDraft) {
|
if (!matched._isDraft) {
|
||||||
// Keep viewId in the URL and strip any leftover filter/sort params
|
// Keep viewId in the URL and strip any leftover filter/sort params
|
||||||
persistView(matchedId)
|
persistView(matchedId)
|
||||||
@ -354,7 +382,8 @@ export const ObjectListViewProvider = ({
|
|||||||
objectType,
|
objectType,
|
||||||
getViewFromUrl,
|
getViewFromUrl,
|
||||||
persistView,
|
persistView,
|
||||||
rememberActiveView
|
rememberActiveView,
|
||||||
|
setObjectListView
|
||||||
])
|
])
|
||||||
|
|
||||||
const selectTab = useCallback(
|
const selectTab = useCallback(
|
||||||
@ -373,7 +402,7 @@ export const ObjectListViewProvider = ({
|
|||||||
)
|
)
|
||||||
if (!view || view._isDraft) return
|
if (!view || view._isDraft) return
|
||||||
|
|
||||||
rememberActiveView(getObjectViewKey(view))
|
rememberActiveView(view)
|
||||||
},
|
},
|
||||||
[activeTabKey, draftViews, isEditing, rememberActiveView, views]
|
[activeTabKey, draftViews, isEditing, rememberActiveView, views]
|
||||||
)
|
)
|
||||||
@ -483,7 +512,7 @@ export const ObjectListViewProvider = ({
|
|||||||
|
|
||||||
if (resolvedTabKey !== ALL_TAB_KEY && savedView && !savedView._isDraft) {
|
if (resolvedTabKey !== ALL_TAB_KEY && savedView && !savedView._isDraft) {
|
||||||
setActiveTabKey(resolvedTabKey)
|
setActiveTabKey(resolvedTabKey)
|
||||||
rememberActiveView(getObjectViewKey(savedView))
|
rememberActiveView(savedView)
|
||||||
} else if (resolvedTabKey !== ALL_TAB_KEY && !savedView) {
|
} else if (resolvedTabKey !== ALL_TAB_KEY && !savedView) {
|
||||||
resolvedTabKey = ALL_TAB_KEY
|
resolvedTabKey = ALL_TAB_KEY
|
||||||
setActiveTabKey(ALL_TAB_KEY)
|
setActiveTabKey(ALL_TAB_KEY)
|
||||||
@ -535,7 +564,7 @@ export const ObjectListViewProvider = ({
|
|||||||
const newKey = String(created._id)
|
const newKey = String(created._id)
|
||||||
setActiveTabKey(newKey)
|
setActiveTabKey(newKey)
|
||||||
applyViewToTable(newKey)
|
applyViewToTable(newKey)
|
||||||
rememberActiveView(getObjectViewKey(created))
|
rememberActiveView(created)
|
||||||
} else {
|
} else {
|
||||||
message.error('Failed to create view')
|
message.error('Failed to create view')
|
||||||
}
|
}
|
||||||
@ -571,7 +600,7 @@ export const ObjectListViewProvider = ({
|
|||||||
(item) => String(item._id) === String(previousKey)
|
(item) => String(item._id) === String(previousKey)
|
||||||
)
|
)
|
||||||
if (view && !view._isDraft) {
|
if (view && !view._isDraft) {
|
||||||
rememberActiveView(getObjectViewKey(view))
|
rememberActiveView(view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
applyViewToTable(previousKey)
|
applyViewToTable(previousKey)
|
||||||
@ -665,7 +694,7 @@ export const ObjectListViewProvider = ({
|
|||||||
(item) => String(item._id) === String(previousKey)
|
(item) => String(item._id) === String(previousKey)
|
||||||
)
|
)
|
||||||
if (view && !view._isDraft) {
|
if (view && !view._isDraft) {
|
||||||
rememberActiveView(getObjectViewKey(view))
|
rememberActiveView(view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
applyViewToTable(previousKey)
|
applyViewToTable(previousKey)
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import {
|
|||||||
createContext,
|
createContext,
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState
|
useState
|
||||||
@ -19,6 +20,7 @@ const getSessionFilterKey = (scope) => `tableState:${scope}:filter`
|
|||||||
const getSessionSortKey = (scope) => `tableState:${scope}:sort`
|
const getSessionSortKey = (scope) => `tableState:${scope}:sort`
|
||||||
const getSessionListFilterKey = (scope) => `tableState:${scope}:listFilter`
|
const getSessionListFilterKey = (scope) => `tableState:${scope}:listFilter`
|
||||||
const getSessionListSortKey = (scope) => `tableState:${scope}:listSort`
|
const getSessionListSortKey = (scope) => `tableState:${scope}:listSort`
|
||||||
|
const getSessionListViewKey = (scope) => `tableState:${scope}:listView`
|
||||||
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
export const getActiveFilterValues = (filterState) => {
|
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) => {
|
const normalizeSorter = (sorter) => {
|
||||||
if (sorter?.field && sorter?.order) {
|
if (sorter?.field && sorter?.order) {
|
||||||
return { field: sorter.field, order: 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.
|
// from the list page to an info page so breadcrumb/neighbors keep working.
|
||||||
const [objectListFilters, setObjectListFilters] = useState({})
|
const [objectListFilters, setObjectListFilters] = useState({})
|
||||||
const [objectListSorters, setObjectListSorters] = 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)
|
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
|
// Keep sequential URL writes in the same tick from clobbering each other
|
||||||
// (e.g. persistView then persistTableState both calling setSearchParams).
|
// (e.g. persistView then persistTableState both calling setSearchParams).
|
||||||
const updateSearchParams = useCallback(
|
const updateSearchParams = useCallback(
|
||||||
(mutator) => {
|
(mutator) => {
|
||||||
|
const previous = searchParamsRef.current.toString()
|
||||||
const next = new URLSearchParams(searchParamsRef.current)
|
const next = new URLSearchParams(searchParamsRef.current)
|
||||||
mutator(next)
|
mutator(next)
|
||||||
|
previousSearchParamsRef.current = previous
|
||||||
searchParamsRef.current = next
|
searchParamsRef.current = next
|
||||||
|
pendingSearchParamsRef.current = next.toString()
|
||||||
setSearchParams(next, { replace: true })
|
setSearchParams(next, { replace: true })
|
||||||
},
|
},
|
||||||
[setSearchParams]
|
[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(
|
const getObjectListFilter = useCallback(
|
||||||
(objectType) => {
|
(objectType) => {
|
||||||
if (!objectType) return {}
|
if (!objectType) return {}
|
||||||
@ -384,6 +463,17 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
[objectListSorters]
|
[objectListSorters]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const getObjectListView = useCallback(
|
||||||
|
(objectType) => {
|
||||||
|
if (!objectType) return null
|
||||||
|
if (objectListViews[objectType]) {
|
||||||
|
return objectListViews[objectType]
|
||||||
|
}
|
||||||
|
return readListViewFromSession(objectType)
|
||||||
|
},
|
||||||
|
[objectListViews]
|
||||||
|
)
|
||||||
|
|
||||||
const hasPageFilter = useCallback(
|
const hasPageFilter = useCallback(
|
||||||
(path) => Object.keys(pageFilters[path] || {}).length > 0,
|
(path) => Object.keys(pageFilters[path] || {}).length > 0,
|
||||||
[pageFilters]
|
[pageFilters]
|
||||||
@ -409,14 +499,17 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
setPageSorter,
|
setPageSorter,
|
||||||
setObjectListFilter,
|
setObjectListFilter,
|
||||||
setObjectListSorter,
|
setObjectListSorter,
|
||||||
|
setObjectListView,
|
||||||
getObjectListFilter,
|
getObjectListFilter,
|
||||||
getObjectListSorter,
|
getObjectListSorter,
|
||||||
|
getObjectListView,
|
||||||
hasPageFilter,
|
hasPageFilter,
|
||||||
hasStoredFilter,
|
hasStoredFilter,
|
||||||
pageFilters,
|
pageFilters,
|
||||||
pageSorters,
|
pageSorters,
|
||||||
objectListFilters,
|
objectListFilters,
|
||||||
objectListSorters
|
objectListSorters,
|
||||||
|
objectListViews
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
getPersistedFilter,
|
getPersistedFilter,
|
||||||
@ -430,14 +523,17 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
setPageSorter,
|
setPageSorter,
|
||||||
setObjectListFilter,
|
setObjectListFilter,
|
||||||
setObjectListSorter,
|
setObjectListSorter,
|
||||||
|
setObjectListView,
|
||||||
getObjectListFilter,
|
getObjectListFilter,
|
||||||
getObjectListSorter,
|
getObjectListSorter,
|
||||||
|
getObjectListView,
|
||||||
hasPageFilter,
|
hasPageFilter,
|
||||||
hasStoredFilter,
|
hasStoredFilter,
|
||||||
pageFilters,
|
pageFilters,
|
||||||
pageSorters,
|
pageSorters,
|
||||||
objectListFilters,
|
objectListFilters,
|
||||||
objectListSorters
|
objectListSorters,
|
||||||
|
objectListViews
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user