[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 { ChevronDown, Circle, type LucideIcon } from 'lucide-react'
|
||||||
import * as Icons from 'lucide-react'
|
import * as Icons from 'lucide-react'
|
||||||
import { useState } from '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 }) {
|
function MenuLeaf({ node, depth }: { node: MenuNode; depth: number }) {
|
||||||
const Icon = getIcon(node.icon)
|
const Icon = getIcon(node.icon)
|
||||||
const path = resolvePath(node.key)
|
const path = resolvePath(node.key)
|
||||||
|
const location = useLocation()
|
||||||
if (!path) return null
|
if (!path) return null
|
||||||
const isDeep = depth >= 2
|
const isDeep = depth >= 2
|
||||||
|
|
||||||
|
const [targetPath, targetQuery = ''] = path.split('?')
|
||||||
|
const isActive = location.pathname === targetPath
|
||||||
|
&& queryMatches(location.search.replace(/^\?/, ''), targetQuery)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavLink
|
<NavLink
|
||||||
to={path}
|
to={path}
|
||||||
// NavLink's default "startsWith" match causes /contracts?type=1 and
|
className={cn(
|
||||||
// /contracts to both highlight. Use `end` for query-param variants.
|
'flex items-center gap-2.5 rounded-md transition',
|
||||||
end={path.includes('?')}
|
isDeep ? 'px-3 py-1 text-[12px]' : 'px-3 py-2 text-sm font-medium',
|
||||||
className={({ isActive }) =>
|
isActive
|
||||||
cn(
|
? 'bg-brand-50 text-brand-700'
|
||||||
'flex items-center gap-2.5 rounded-md transition',
|
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900',
|
||||||
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')} />
|
<Icon className={cn(isDeep ? 'h-3.5 w-3.5' : 'h-4 w-4')} />
|
||||||
{node.label}
|
{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 }) {
|
function MenuLeaf({ node, depth }: { node: MenuNode; depth: number }) {
|
||||||
const Icon = getIcon(node.icon)
|
const Icon = getIcon(node.icon)
|
||||||
const path = resolvePath(node.key)
|
const path = resolvePath(node.key)
|
||||||
|
const location = useLocation()
|
||||||
if (!path) return null
|
if (!path) return null
|
||||||
const isDeep = depth >= 2
|
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 (
|
return (
|
||||||
<NavLink
|
<NavLink
|
||||||
to={path}
|
to={path}
|
||||||
end={path.includes('?')}
|
className={cn(
|
||||||
className={({ isActive }) =>
|
'flex items-center gap-2.5 rounded-md transition',
|
||||||
cn(
|
isDeep ? 'px-3 py-1 text-[12px]' : 'px-3 py-2 text-sm font-medium',
|
||||||
'flex items-center gap-2.5 rounded-md transition',
|
isActive
|
||||||
isDeep ? 'px-3 py-1 text-[12px]' : 'px-3 py-2 text-sm font-medium',
|
? 'bg-brand-50 text-brand-700'
|
||||||
isActive
|
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900',
|
||||||
? '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')} />
|
<Icon className={cn(isDeep ? 'h-3.5 w-3.5' : 'h-4 w-4')} />
|
||||||
{node.label}
|
{node.label}
|
||||||
|
|||||||
Reference in New Issue
Block a user