Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 81 additions & 26 deletions src/src/components/navbar/side-nav-bar-mobile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,31 @@ import {
} from '@/components/ui/dropdown-menu'
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
import { AdminMenus } from '@/config'
import { ChevronDown, ChevronRight, CircleUser, Menu, Package2, Settings, User, LogOut } from 'lucide-react'
import { useCurrentUser } from '@/lib/hooks/useCurrentUser'
import { useGrantedPolicies } from '@/lib/hooks/useGrantedPolicies'
import { USER_ROLE } from '@/lib/utils'
import useSession from '@/useSession'
import {
ChevronDown,
ChevronRight,
CircleUser,
LogOut,
Menu,
Package2,
Settings,
User,
} from 'lucide-react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useState } from 'react'
import { useMemo, useState } from 'react'
import ClientLink from '../ui/client-link'
import useSession from '@/useSession'

export default function SideNavBarMobile() {
const pathname = usePathname()
const sessionData = useSession()
const { can } = useGrantedPolicies()
const currentUser = useCurrentUser()
const isAdmin = currentUser?.roles?.includes(USER_ROLE.ADMIN) ?? false
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isAdmin variable is computed but never used in the component logic. It only appears in the JSX for displaying the admin badge. Consider removing it if the badge is purely cosmetic, or use it in the permission logic if admin users should bypass permission checks.

Suggested change
const isAdmin = currentUser?.roles?.includes(USER_ROLE.ADMIN) ?? false

Copilot uses AI. Check for mistakes.

// Initialize expanded menus based on current path
const getInitialExpandedMenus = () => {
Expand All @@ -36,6 +51,29 @@ export default function SideNavBarMobile() {

const [expandedMenus, setExpandedMenus] = useState<Set<string>>(getInitialExpandedMenus())

// Filter menus based on permissions
const visibleMenus = useMemo(() => {
return AdminMenus.filter((menu) => {
// If menu has policy, check permission
if (menu.policy && !can(menu.policy)) {
return false
}

// If menu has submenus, check if at least one submenu is visible
if (menu.submenus && menu.submenus.length > 0) {
const visibleSubmenus = menu.submenus.filter((submenu) => {
// If submenu has policy, check permission; otherwise show it
return !submenu.policy || can(submenu.policy)
})
// Show parent menu only if at least one submenu is visible
return visibleSubmenus.length > 0
}

// Menu without policy or with permission is visible
return true
})
}, [can])
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useMemo dependency array is incomplete. It should include 'AdminMenus' to recompute when the menu configuration changes. Add AdminMenus to the dependencies: }, [can, AdminMenus])

Suggested change
}, [can])
}, [can, AdminMenus])

Copilot uses AI. Check for mistakes.

const toggleMenu = (menuName: string) => {
const newExpandedMenus = new Set(expandedMenus)
if (newExpandedMenus.has(menuName)) {
Expand Down Expand Up @@ -74,18 +112,25 @@ export default function SideNavBarMobile() {
<span className="font-bold text-lg bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
AbpReact
</span>
<span className="text-xs bg-primary/10 text-primary px-2 py-1 rounded-full font-medium">
Admin
</span>
{isAdmin && (
<span className="text-xs bg-primary/10 text-primary px-2 py-1 rounded-full font-medium">
Admin
</span>
)}
</div>

{/* Mobile Navigation */}
<nav className="flex-1 space-y-2">
{AdminMenus.map((menu) => {
{visibleMenus.map((menu) => {
const isActive = isMenuActive(menu.link, menu.submenus)
const isExpanded = expandedMenus.has(menu.name)
const hasSubmenus = menu.submenus && menu.submenus.length > 0

// Filter visible submenus based on permissions
const visibleSubmenus = hasSubmenus
? menu.submenus!.filter((submenu) => !submenu.policy || can(submenu.policy))
: []

return (
<div key={menu.name}>
<div className="flex items-center justify-between">
Expand All @@ -101,16 +146,20 @@ export default function SideNavBarMobile() {
}`}
>
{menu.icon && (
<menu.icon className={`h-4 w-4 transition-colors ${
isActive ? 'text-primary' : 'text-muted-foreground group-hover:text-primary'
}`} />
<menu.icon
className={`h-4 w-4 transition-colors ${
isActive
? 'text-primary'
: 'text-muted-foreground group-hover:text-primary'
}`}
/>
)}
<span className="font-medium">{menu.name}</span>
{isActive && (
<div className="ml-auto w-1.5 h-1.5 bg-primary rounded-full"></div>
)}
</Link>
{hasSubmenus && (
{hasSubmenus && visibleSubmenus.length > 0 && (
<button
onClick={() => toggleMenu(menu.name)}
className="p-1 hover:bg-accent rounded-sm transition-colors"
Expand All @@ -124,22 +173,28 @@ export default function SideNavBarMobile() {
)}
</div>

{hasSubmenus && isExpanded && (
{hasSubmenus && isExpanded && visibleSubmenus.length > 0 && (
<div className="ml-6 mt-1 space-y-1">
{menu.submenus!.map((submenu, subIndex) => {
{visibleSubmenus.map((submenu, subIndex) => {
const isSubmenuActive = pathname === submenu.link
return (
<Link
key={subIndex}
href={submenu.link}
className={`flex items-center gap-3 rounded-lg px-3 py-2 text-muted-foreground transition-all hover:text-primary hover:bg-primary/5 text-sm group ${
isSubmenuActive ? 'text-primary bg-primary/10 border border-primary/20' : ''
isSubmenuActive
? 'text-primary bg-primary/10 border border-primary/20'
: ''
}`}
>
{submenu.icon && (
<submenu.icon className={`h-3.5 w-3.5 transition-colors ${
isSubmenuActive ? 'text-primary' : 'text-muted-foreground group-hover:text-primary'
}`} />
<submenu.icon
className={`h-3.5 w-3.5 transition-colors ${
isSubmenuActive
? 'text-primary'
: 'text-muted-foreground group-hover:text-primary'
}`}
/>
)}
<span className="font-medium">{submenu.name}</span>
{isSubmenuActive && (
Expand Down Expand Up @@ -168,14 +223,14 @@ export default function SideNavBarMobile() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
<div className="flex items-center justify-start gap-2 p-2">
<div className="flex flex-col space-y-1 leading-none">
<p className="font-medium">{sessionData.data?.userInfo?.name || 'Admin User'}</p>
<p className="w-[200px] truncate text-sm text-muted-foreground">
{sessionData.data?.userInfo?.email || 'No email available'}
</p>
</div>
</div>
<div className="flex items-center justify-start gap-2 p-2">
<div className="flex flex-col space-y-1 leading-none">
<p className="font-medium">{sessionData.data?.userInfo?.name || 'Admin User'}</p>
<p className="w-[200px] truncate text-sm text-muted-foreground">
{sessionData.data?.userInfo?.email || 'No email available'}
</p>
</div>
</div>
<DropdownMenuSeparator />
<Link href="/admin" className="cursor-pointer">
<DropdownMenuItem className="flex items-center gap-2">
Expand Down
52 changes: 44 additions & 8 deletions src/src/components/navbar/side-nav-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ import { AdminMenus } from '@/config'
import { ChevronDown, ChevronRight, Package2 } from 'lucide-react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useState } from 'react'
import { useState, useMemo } from 'react'
import { useGrantedPolicies } from '@/lib/hooks/useGrantedPolicies'
import { useCurrentUser } from '@/lib/hooks/useCurrentUser'
import { USER_ROLE } from '@/lib/utils'

export default function SideBarMenu() {
const pathname = usePathname()
const { can } = useGrantedPolicies()
const currentUser = useCurrentUser()
const isAdmin = currentUser?.roles?.includes(USER_ROLE.ADMIN) ?? false
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isAdmin variable is computed but never used in the component logic. It only appears in the JSX for displaying the admin badge. Consider removing it if the badge is purely cosmetic, or use it in the permission logic if admin users should bypass permission checks.

Suggested change
const isAdmin = currentUser?.roles?.includes(USER_ROLE.ADMIN) ?? false

Copilot uses AI. Check for mistakes.

// Initialize expanded menus based on current path
const getInitialExpandedMenus = () => {
Expand All @@ -24,6 +30,29 @@ export default function SideBarMenu() {

const [expandedMenus, setExpandedMenus] = useState<Set<string>>(getInitialExpandedMenus())

// Filter menus based on permissions
const visibleMenus = useMemo(() => {
return AdminMenus.filter((menu) => {
// If menu has policy, check permission
if (menu.policy && !can(menu.policy)) {
return false
}

// If menu has submenus, check if at least one submenu is visible
if (menu.submenus && menu.submenus.length > 0) {
const visibleSubmenus = menu.submenus.filter((submenu) => {
// If submenu has policy, check permission; otherwise show it
return !submenu.policy || can(submenu.policy)
})
// Show parent menu only if at least one submenu is visible
return visibleSubmenus.length > 0
}

// Menu without policy or with permission is visible
return true
})
}, [can])
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useMemo dependency array is incomplete. It should include 'AdminMenus' to recompute when the menu configuration changes. Add AdminMenus to the dependencies: }, [can, AdminMenus])

Suggested change
}, [can])
}, [can, AdminMenus])

Copilot uses AI. Check for mistakes.

const toggleMenu = (menuName: string) => {
const newExpandedMenus = new Set(expandedMenus)
if (newExpandedMenus.has(menuName)) {
Expand Down Expand Up @@ -54,18 +83,25 @@ export default function SideBarMenu() {
<span className="font-bold bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
AbpReact
</span>
<span className="text-xs bg-primary/10 text-primary px-2 py-1 rounded-full font-medium">
Admin
</span>
{isAdmin && (
<span className="text-xs bg-primary/10 text-primary px-2 py-1 rounded-full font-medium">
Admin
</span>
)}
</Link>
</div>
<div className="flex-1">
<nav className="grid items-start px-2 text-sm font-medium lg:px-4">
{AdminMenus.map((menu, index) => {
{visibleMenus.map((menu, index) => {
const isActive = isMenuActive(menu.link, menu.submenus)
const isExpanded = expandedMenus.has(menu.name)
const hasSubmenus = menu.submenus && menu.submenus.length > 0

// Filter visible submenus based on permissions
const visibleSubmenus = hasSubmenus
? menu.submenus!.filter((submenu) => !submenu.policy || can(submenu.policy))
: []

return (
<div key={index}>
<div className="flex items-center justify-between">
Expand All @@ -90,7 +126,7 @@ export default function SideBarMenu() {
<div className="ml-auto w-1.5 h-1.5 bg-primary rounded-full"></div>
)}
</Link>
{hasSubmenus && (
{hasSubmenus && visibleSubmenus.length > 0 && (
<button
onClick={() => toggleMenu(menu.name)}
className="p-1 hover:bg-accent rounded-sm transition-colors"
Expand All @@ -104,9 +140,9 @@ export default function SideBarMenu() {
)}
</div>

{hasSubmenus && isExpanded && (
{hasSubmenus && isExpanded && visibleSubmenus.length > 0 && (
<div className="ml-6 mt-1 space-y-1">
{menu.submenus!.map((submenu, subIndex) => {
{visibleSubmenus.map((submenu, subIndex) => {
const isSubmenuActive = pathname === submenu.link
return (
<Link
Expand Down
15 changes: 7 additions & 8 deletions src/src/components/role/RoleList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { Search } from '../ui/Search'
import { useToast } from '../ui/use-toast'
import { DeleteRole } from './DeleteRole'
import { RoleEdit } from './RoleEdit'
import { RolePermission } from './RolePermission'

export const RoleList = () => {
const { toast } = useToast()
Expand Down Expand Up @@ -54,13 +53,13 @@ export const RoleList = () => {
return (
<PermissionActions
actions={[
{
icon: 'permission',
policy: Permissions.ROLES_MANAGE_PERMISSIONS,
callback: () => {
window.location.href = `/admin/permissions/role/${info.row.original.name}`
},
},
{
icon: 'permission',
policy: Permissions.ROLES_MANAGE_PERMISSIONS,
callback: () => {
window.location.href = `/admin/permissions/role/${info.row.original.name}`
},
},
{
icon: 'pencil',
policy: Permissions.ROLES_UPDATE,
Expand Down
19 changes: 16 additions & 3 deletions src/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Cog, Database, FileText, Home, Menu, MessageSquare, UserRound, Users } from 'lucide-react'
import React from 'react'
import { Policy } from '@/lib/hooks/useGrantedPolicies'
import { Permissions } from '@/lib/utils'

/**
* Configuration for the OpenID client.
Expand Down Expand Up @@ -38,63 +40,74 @@ export const PublicMenus: Array<{ Name: string; Link: string }> = [

/**
* List of menus shown in the Admin layout.
* Each menu item contains a name, link, and icon.
* Each menu item contains a name, link, icon, and optional policy for permission control.
* Supports nested submenus for 2-level navigation.
* If policy is not specified, the menu item will be shown to all authenticated users.
*
* @type {Array<{name: string, link: string, icon: React.ComponentType<{className?: string}>, submenus?: Array<{name: string, link: string, icon: React.ComponentType<{className?: string}>}>}>}
* @type {Array<{name: string, link: string, icon: React.ComponentType<{className?: string}>, policy?: Policy, submenus?: Array<{name: string, link: string, icon: React.ComponentType<{className?: string}>, policy?: Policy}>}>}
*/
export const AdminMenus: Array<{
name: string
link: string
icon: React.ComponentType<{ className?: string }>
submenus?: Array<{ name: string; link: string; icon: React.ComponentType<{ className?: string }> }>
policy?: Policy
submenus?: Array<{ name: string; link: string; icon: React.ComponentType<{ className?: string }>; policy?: Policy }>
}> = [
{
name: 'Home',
link: '/admin',
icon: Home,
// No policy required - all authenticated users can access home
},
{
name: 'Users',
link: '/admin/users',
icon: UserRound,
policy: Permissions.USERS,
},
{
name: 'Roles',
link: '/admin/users/roles',
icon: Users,
policy: Permissions.ROLES,
},
{
name: 'CMS',
link: '/admin/cms',
icon: FileText,
// Parent menu doesn't need policy, but submenus do
submenus: [
{
name: 'Pages',
link: '/admin/cms/pages',
icon: FileText,
policy: Permissions.CMSKIT_PAGES,
},
{
name: 'Menu Items',
link: '/admin/cms/menus',
icon: Menu,
policy: Permissions.CMSKIT_MENUS,
},
{
name: 'Comments',
link: '/admin/cms/comments',
icon: MessageSquare,
policy: Permissions.CMSKIT_COMMENTS,
},
],
},
{
name: 'Tenants',
link: '/admin/tenants',
icon: Database,
policy: Permissions.TENANTS,
},
{
name: 'Settings',
link: '/admin/settings',
icon: Cog,
policy: Permissions.SETTINGS_EMAILING,
},
]