From fc4b3d6078b169a40a0e443db9fdae2faaf16a8a Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Fri, 24 Apr 2026 11:23:56 +0700 Subject: [PATCH] [CLAUDE] FE: NavLink active check query string (khong chi pathname) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- fe-admin/src/components/Layout.tsx | 38 ++++++++++++++++++++---------- fe-user/src/components/Layout.tsx | 38 ++++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 23 deletions(-) diff --git a/fe-admin/src/components/Layout.tsx b/fe-admin/src/components/Layout.tsx index 6d87632..36bd219 100644 --- a/fe-admin/src/components/Layout.tsx +++ b/fe-admin/src/components/Layout.tsx @@ -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 ( - 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', - ) - } + 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', + )} > {node.label} diff --git a/fe-user/src/components/Layout.tsx b/fe-user/src/components/Layout.tsx index 35c4aff..0e1e188 100644 --- a/fe-user/src/components/Layout.tsx +++ b/fe-user/src/components/Layout.tsx @@ -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 ( - 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', - ) - } + 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', + )} > {node.label}