[CLAUDE] FE-Admin FE-User: Chunk U — Sidebar truncate long label + tooltip (Mig 27 DisplayLabel dài wrap fix)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m30s

Bro UAT screenshot 2026-05-15: Submenu "1. Duyệt Nhà Cung Cấp - Thầu phụ
(NCC -TP)" trong sidebar fe-user wrap 2 dòng (label dài ~50 chars vs
sidebar w-60 = 240px chỉ fit ~25 chars).

Root: Admin đã set DisplayLabel custom qua Mig 27 (S20 t7 Menu eOffice
admin page) — `MenuItems.Pe_DuyetNcc` DisplayLabel = "1. Duyệt Nhà Cung
Cấp - Thầu phụ (NCC -TP)" (Label gốc = "Duyệt NCC" ngắn). FE render
{effectiveLabel(node)} thẳng vào span flex KHÔNG có truncate.

Fix Plan U mirror 2 app (rule §3.9):

3 nơi render label trong fe-user/Layout.tsx + 2 nơi mirror fe-admin:
1. MenuNodeRenderer button (accordion toggle):
   ```diff
   - <span className="flex items-center gap-2">
   + <span className="flex min-w-0 flex-1 items-center gap-2">
   -   <Icon className="h-4 w-4" />
   -   {effectiveLabel(node)}
   +   <Icon className="h-4 w-4 shrink-0" />
   +   <span className="truncate" title={effectiveLabel(node)}>{effectiveLabel(node)}</span>
     </span>
   - <ChevronDown ... transition />
   + <ChevronDown ... shrink-0 ... transition />
   ```

2. MenuLeaf NavLink:
   ```diff
   - <NavLink to={path} className={cn('flex items-center gap-2.5...')}>
   + <NavLink to={path} title={effectiveLabel(node)} className={cn('flex min-w-0 items-center gap-2.5...')}>
   -   <Icon className={cn(isDeep ? 'h-3.5 w-3.5' : 'h-4 w-4')} />
   -   {effectiveLabel(node)}
   +   <Icon className={cn('shrink-0', isDeep ? 'h-3.5 w-3.5' : 'h-4 w-4')} />
   +   <span className="truncate">{effectiveLabel(node)}</span>
     </NavLink>
   ```

3. StaticLeaf NavLink (fe-user only — Hộp thư static entry):
   Pattern tương tự MenuLeaf

fe-admin dùng `node.label` thay vì `effectiveLabel(node)` (admin sidebar
luôn show Label gốc, KHÔNG đụng DisplayLabel per S20 t7 Q2=b).

Pattern key:
- `min-w-0 flex-1` trên flex parent — cần thiết để truncate child shrink
- `shrink-0` trên Icon + ChevronDown — giữ size không co
- `truncate` (Tailwind = overflow-hidden text-ellipsis whitespace-nowrap) trên span text
- `title={label}` tooltip hover show full label nếu user cần đọc đầy đủ

Verify:
- npm run build fe-user PASS 16.79s clean
- npm run build fe-admin PASS 8.16s clean
- 0 TS error

KHÔNG đụng BE. Admin tự control DisplayLabel qua Mig 27 Menu eOffice page
— Plan U chỉ ensure FE render gracefully với label dài (truncate +
tooltip hover) thay vì wrap broken visual.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-15 15:33:17 +07:00
parent 7b7b28f2cd
commit 86d8806afc
2 changed files with 25 additions and 17 deletions

View File

@ -141,11 +141,13 @@ function MenuGroup({ node, depth }: { node: MenuNode; depth: number }) {
: 'px-3 py-1.5 text-[13px] font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900', : 'px-3 py-1.5 text-[13px] font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900',
)} )}
> >
<span className="flex items-center gap-2"> {/* Plan U S23 t11 — truncate label dài + tooltip hover (mirror fe-user
<Icon className="h-4 w-4" /> rule §3.9). min-w-0 cần thiết cho truncate flex child. */}
{node.label} <span className="flex min-w-0 flex-1 items-center gap-2">
<Icon className="h-4 w-4 shrink-0" />
<span className="truncate" title={node.label}>{node.label}</span>
</span> </span>
<ChevronDown className={cn('h-3.5 w-3.5 text-slate-400 transition', !open && '-rotate-90')} /> <ChevronDown className={cn('h-3.5 w-3.5 shrink-0 text-slate-400 transition', !open && '-rotate-90')} />
</button> </button>
{open && ( {open && (
<div className={cn( <div className={cn(
@ -193,16 +195,17 @@ function MenuLeaf({ node, depth }: { node: MenuNode; depth: number }) {
return ( return (
<NavLink <NavLink
to={path} to={path}
title={node.label}
className={cn( className={cn(
'flex items-center gap-2.5 rounded-md transition', 'flex min-w-0 items-center gap-2.5 rounded-md transition',
isDeep ? 'px-3 py-1 text-[12px]' : 'px-3 py-2 text-sm font-medium', isDeep ? 'px-3 py-1 text-[12px]' : 'px-3 py-2 text-sm font-medium',
isActive isActive
? 'bg-brand-50 text-brand-700' ? 'bg-brand-50 text-brand-700'
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900', : '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('shrink-0', isDeep ? 'h-3.5 w-3.5' : 'h-4 w-4')} />
{node.label} <span className="truncate">{node.label}</span>
</NavLink> </NavLink>
) )
} }

View File

@ -180,11 +180,14 @@ function MenuGroup({ node, depth }: { node: MenuNode; depth: number }) {
isAccordion && open && 'bg-slate-50 text-slate-900', isAccordion && open && 'bg-slate-50 text-slate-900',
)} )}
> >
<span className="flex items-center gap-2"> {/* Plan U S23 t11 — truncate label dài + tooltip hover (admin Mig 27
<Icon className="h-4 w-4" /> có thể đặt DisplayLabel dài). min-w-0 cần thiết để truncate hoạt
{effectiveLabel(node)} động trong flex child. shrink-0 giữ icon + chevron không co. */}
<span className="flex min-w-0 flex-1 items-center gap-2">
<Icon className="h-4 w-4 shrink-0" />
<span className="truncate" title={effectiveLabel(node)}>{effectiveLabel(node)}</span>
</span> </span>
<ChevronDown className={cn('h-3.5 w-3.5 text-slate-400 transition', !open && '-rotate-90')} /> <ChevronDown className={cn('h-3.5 w-3.5 shrink-0 text-slate-400 transition', !open && '-rotate-90')} />
</button> </button>
{open && ( {open && (
<div className={cn( <div className={cn(
@ -235,16 +238,17 @@ function MenuLeaf({ node, depth }: { node: MenuNode; depth: number }) {
return ( return (
<NavLink <NavLink
to={path} to={path}
title={effectiveLabel(node)}
className={cn( className={cn(
'flex items-center gap-2.5 rounded-md transition', 'flex min-w-0 items-center gap-2.5 rounded-md transition',
isDeep ? 'px-3 py-1 text-[12px]' : 'px-3 py-2 text-sm font-medium', isDeep ? 'px-3 py-1 text-[12px]' : 'px-3 py-2 text-sm font-medium',
isActive isActive
? 'bg-brand-50 text-brand-700' ? 'bg-brand-50 text-brand-700'
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900', : '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('shrink-0', isDeep ? 'h-3.5 w-3.5' : 'h-4 w-4')} />
{effectiveLabel(node)} <span className="truncate">{effectiveLabel(node)}</span>
</NavLink> </NavLink>
) )
} }
@ -268,15 +272,16 @@ function StaticLeaf({ node }: { node: MenuNode }) {
<NavLink <NavLink
to={path} to={path}
end end
title={effectiveLabel(node)}
className={({ isActive }) => className={({ isActive }) =>
cn( cn(
'flex items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium transition', 'flex min-w-0 items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium transition',
isActive ? 'bg-brand-50 text-brand-700' : 'text-slate-600 hover:bg-slate-100 hover:text-slate-900', isActive ? 'bg-brand-50 text-brand-700' : 'text-slate-600 hover:bg-slate-100 hover:text-slate-900',
) )
} }
> >
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4 shrink-0" />
{effectiveLabel(node)} <span className="truncate">{effectiveLabel(node)}</span>
</NavLink> </NavLink>
) )
} }