feat: enhance sidebar navigation with localStorage state management and section key support

This commit is contained in:
mlogclub
2026-05-30 20:06:34 +08:00
parent 6594140684
commit edb8355aad
4 changed files with 62 additions and 3 deletions
+1
View File
@@ -66,6 +66,7 @@ export function AppSidebar({ ...props }: ComponentProps<typeof Sidebar>) {
<NavMain <NavMain
key={section.titleKey} key={section.titleKey}
icon={section.icon} icon={section.icon}
sectionKey={section.titleKey}
title={t(section.titleKey)} title={t(section.titleKey)}
items={section.items.map((item) => ({ items={section.items.map((item) => ({
...item, ...item,
+25 -3
View File
@@ -7,7 +7,9 @@ import { useEffect, useState } from "react"
import { import {
dashboardNavSectionHasActiveItem, dashboardNavSectionHasActiveItem,
getDashboardNavSectionStorageKey,
isDashboardNavItemActive, isDashboardNavItemActive,
parseDashboardNavSectionOpenState,
} from "@/lib/navigation-active" } from "@/lib/navigation-active"
import { import {
Collapsible, Collapsible,
@@ -26,10 +28,12 @@ import {
export function NavMain({ export function NavMain({
icon, icon,
sectionKey,
title, title,
items, items,
}: { }: {
icon?: React.ReactNode icon?: React.ReactNode
sectionKey: string
title: string title: string
items: ReadonlyArray<{ items: ReadonlyArray<{
title: string title: string
@@ -39,20 +43,38 @@ export function NavMain({
}) { }) {
const pathname = usePathname() const pathname = usePathname()
const hasActiveItem = dashboardNavSectionHasActiveItem(items, pathname) const hasActiveItem = dashboardNavSectionHasActiveItem(items, pathname)
const [open, setOpen] = useState(true) const storageKey = getDashboardNavSectionStorageKey(sectionKey)
const [open, setOpen] = useState(() => {
if (typeof window === "undefined") {
return true
}
return parseDashboardNavSectionOpenState(window.localStorage.getItem(storageKey)) ?? true
})
useEffect(() => { useEffect(() => {
const storedOpen = parseDashboardNavSectionOpenState(
window.localStorage.getItem(storageKey)
)
if (storedOpen !== undefined) {
setOpen(storedOpen)
return
}
if (hasActiveItem) { if (hasActiveItem) {
setOpen(true) setOpen(true)
} }
}, [hasActiveItem]) }, [hasActiveItem, storageKey])
const handleOpenChange = (nextOpen: boolean) => {
setOpen(nextOpen)
window.localStorage.setItem(storageKey, String(nextOpen))
}
return ( return (
<SidebarGroup className="px-2 py-0 first:pt-2 last:pb-2"> <SidebarGroup className="px-2 py-0 first:pt-2 last:pb-2">
<SidebarMenu> <SidebarMenu>
<Collapsible <Collapsible
open={open} open={open}
onOpenChange={setOpen} onOpenChange={handleOpenChange}
className="group/collapsible" className="group/collapsible"
render={<SidebarMenuItem />} render={<SidebarMenuItem />}
> >
+20
View File
@@ -63,3 +63,23 @@ describe("dashboardNavSectionHasActiveItem", () => {
assert.equal(dashboardNavSectionHasActiveItem(items, "/dashboard/users"), false) assert.equal(dashboardNavSectionHasActiveItem(items, "/dashboard/users"), false)
}) })
}) })
describe("dashboard nav section storage helpers", () => {
it("builds stable localStorage keys from section identifiers", async () => {
const { getDashboardNavSectionStorageKey } = await loadModule()
assert.equal(
getDashboardNavSectionStorageKey("nav.receptionCenter"),
"dashboard.sidebar.navSection.nav.receptionCenter.open"
)
})
it("parses stored open states and ignores unknown values", async () => {
const { parseDashboardNavSectionOpenState } = await loadModule()
assert.equal(parseDashboardNavSectionOpenState("true"), true)
assert.equal(parseDashboardNavSectionOpenState("false"), false)
assert.equal(parseDashboardNavSectionOpenState(null), undefined)
assert.equal(parseDashboardNavSectionOpenState("bad"), undefined)
})
})
+16
View File
@@ -2,6 +2,8 @@ export type DashboardNavActiveItem = {
url: string url: string
} }
const DASHBOARD_NAV_SECTION_STORAGE_PREFIX = "dashboard.sidebar.navSection"
function normalizePath(path: string) { function normalizePath(path: string) {
return path !== "/" ? path.replace(/\/+$/, "") : path return path !== "/" ? path.replace(/\/+$/, "") : path
} }
@@ -22,3 +24,17 @@ export function dashboardNavSectionHasActiveItem(
) { ) {
return items.some((item) => isDashboardNavItemActive(pathname, item.url)) return items.some((item) => isDashboardNavItemActive(pathname, item.url))
} }
export function getDashboardNavSectionStorageKey(sectionKey: string) {
return `${DASHBOARD_NAV_SECTION_STORAGE_PREFIX}.${sectionKey}.open`
}
export function parseDashboardNavSectionOpenState(value: string | null) {
if (value === "true") {
return true
}
if (value === "false") {
return false
}
return undefined
}