[CLAUDE] FE: NavLink active check query string (khong chi pathname)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m51s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m51s
Bug: click leaf 'Duyet' (/purchase-evaluations?type=2&pendingMe=1) khien leaf 'Danh sach' (/purchase-evaluations?type=2) cung highlight cung luc. Nguyen nhan: NavLink 'end' prop chi match pathname. 2 leaf cung pathname /purchase-evaluations → ca 2 active. Fix: custom isActive voi queryMatches helper — compare query string dang key-value set (thu tu param khong quan trong). 2 leaf chi active khi pathname + query dung khop. Dong bo ca fe-admin + fe-user. Anh huong tat ca menu leaf co ?query= variants: Ct_* (Danh sach /contracts?type=N vs Duyet /contracts?type=N& pendingMe=1), Pe_* (tuong tu /purchase-evaluations), admin workflow leaf Wf_* + PeWf_* (khong dinh vi path khong query params).
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import { Link, NavLink, Outlet } from 'react-router-dom'
|
||||
import { Link, NavLink, Outlet, useLocation } from 'react-router-dom'
|
||||
import { ChevronDown, Circle, type LucideIcon } from 'lucide-react'
|
||||
import * as Icons from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
@ -142,27 +142,39 @@ function MenuGroup({ node, depth }: { node: MenuNode; depth: number }) {
|
||||
)
|
||||
}
|
||||
|
||||
// So sánh 2 query string dạng key-value set (thứ tự param không quan trọng).
|
||||
// Fix bug: /contracts?type=1 và ?type=1&pendingMe=1 cùng highlight vì NavLink
|
||||
// built-in `end` prop chỉ match pathname, không check query string.
|
||||
function queryMatches(current: string, target: string): boolean {
|
||||
const a = new URLSearchParams(current)
|
||||
const b = new URLSearchParams(target)
|
||||
const aKeys = [...a.keys()].sort()
|
||||
const bKeys = [...b.keys()].sort()
|
||||
if (aKeys.length !== bKeys.length) return false
|
||||
return aKeys.every((k, i) => bKeys[i] === k && a.get(k) === b.get(k))
|
||||
}
|
||||
|
||||
function MenuLeaf({ node, depth }: { node: MenuNode; depth: number }) {
|
||||
const Icon = getIcon(node.icon)
|
||||
const path = resolvePath(node.key)
|
||||
const location = useLocation()
|
||||
if (!path) return null
|
||||
const isDeep = depth >= 2
|
||||
|
||||
const [targetPath, targetQuery = ''] = path.split('?')
|
||||
const isActive = location.pathname === targetPath
|
||||
&& queryMatches(location.search.replace(/^\?/, ''), targetQuery)
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
to={path}
|
||||
// NavLink's default "startsWith" match causes /contracts?type=1 and
|
||||
// /contracts to both highlight. Use `end` for query-param variants.
|
||||
end={path.includes('?')}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
className={cn(
|
||||
'flex items-center gap-2.5 rounded-md transition',
|
||||
isDeep ? 'px-3 py-1 text-[12px]' : 'px-3 py-2 text-sm font-medium',
|
||||
isActive
|
||||
? 'bg-brand-50 text-brand-700'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900',
|
||||
)
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Icon className={cn(isDeep ? 'h-3.5 w-3.5' : 'h-4 w-4')} />
|
||||
{node.label}
|
||||
|
||||
@ -185,25 +185,43 @@ function MenuGroup({ node, depth }: { node: MenuNode; depth: number }) {
|
||||
)
|
||||
}
|
||||
|
||||
// So sánh 2 query string dạng key-value set (thứ tự param không quan trọng).
|
||||
// Dùng để distinguish /path?type=2 vs /path?type=2&pendingMe=1 — NavLink isActive
|
||||
// built-in chỉ match pathname, không check query string.
|
||||
function queryMatches(current: string, target: string): boolean {
|
||||
const a = new URLSearchParams(current)
|
||||
const b = new URLSearchParams(target)
|
||||
const aKeys = [...a.keys()].sort()
|
||||
const bKeys = [...b.keys()].sort()
|
||||
if (aKeys.length !== bKeys.length) return false
|
||||
return aKeys.every((k, i) => bKeys[i] === k && a.get(k) === b.get(k))
|
||||
}
|
||||
|
||||
function MenuLeaf({ node, depth }: { node: MenuNode; depth: number }) {
|
||||
const Icon = getIcon(node.icon)
|
||||
const path = resolvePath(node.key)
|
||||
const location = useLocation()
|
||||
if (!path) return null
|
||||
const isDeep = depth >= 2
|
||||
|
||||
// Custom active check: pathname match + query string match (set equality).
|
||||
// Fix bug: /purchase-evaluations?type=2 và ?type=2&pendingMe=1 cùng highlight
|
||||
// vì NavLink default chỉ check pathname.
|
||||
const [targetPath, targetQuery = ''] = path.split('?')
|
||||
const pathnameMatches = location.pathname === targetPath
|
||||
const qMatches = queryMatches(location.search.replace(/^\?/, ''), targetQuery)
|
||||
const isActive = pathnameMatches && qMatches
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
to={path}
|
||||
end={path.includes('?')}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
className={cn(
|
||||
'flex items-center gap-2.5 rounded-md transition',
|
||||
isDeep ? 'px-3 py-1 text-[12px]' : 'px-3 py-2 text-sm font-medium',
|
||||
isActive
|
||||
? 'bg-brand-50 text-brand-700'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900',
|
||||
)
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Icon className={cn(isDeep ? 'h-3.5 w-3.5' : 'h-4 w-4')} />
|
||||
{node.label}
|
||||
|
||||
Reference in New Issue
Block a user