fix notifications
Build & Push / Pipeline Tests (push) Failing after 1m8s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Successful in 53s
Test / API Unit Tests (push) Failing after 52s
Test / Homepage Unit Tests (push) Successful in 46s
Test / Carplace Unit Tests (push) Successful in 43s
Test / Admin Unit Tests (push) Successful in 43s
Test / Dashboard Unit Tests (push) Successful in 46s
Test / API Integration Tests (push) Successful in 1m7s
Build & Push / Pipeline Tests (push) Failing after 1m8s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Successful in 53s
Test / API Unit Tests (push) Failing after 52s
Test / Homepage Unit Tests (push) Successful in 46s
Test / Carplace Unit Tests (push) Successful in 43s
Test / Admin Unit Tests (push) Successful in 43s
Test / Dashboard Unit Tests (push) Successful in 46s
Test / API Integration Tests (push) Successful in 1m7s
This commit is contained in:
@@ -2,14 +2,14 @@ import { describe, expect, it } from 'vitest'
|
||||
import { APPROVED_BASELINE_MENU_KEYS, getEmployeeLogoutRedirectUrl, hasRenderableMenuItems, shouldUseGeneratedMenu } from './Sidebar'
|
||||
|
||||
describe('Sidebar menu rendering helpers', () => {
|
||||
it('keeps the safe fallback menu to the approved seven baseline items in order', () => {
|
||||
it('keeps dashboard first, then sorts the remaining safe fallback items alphabetically', () => {
|
||||
expect(APPROVED_BASELINE_MENU_KEYS).toEqual([
|
||||
'dashboard',
|
||||
'reservations',
|
||||
'fleet',
|
||||
'customers',
|
||||
'reports',
|
||||
'billing',
|
||||
'customers',
|
||||
'fleet',
|
||||
'reports',
|
||||
'reservations',
|
||||
'settings',
|
||||
])
|
||||
})
|
||||
|
||||
@@ -101,7 +101,13 @@ const OWNER_SYSTEM_NAV_ITEMS = [
|
||||
{ href: '/subscription', key: 'subscription', icon: 'CreditCard', minRole: 'OWNER' },
|
||||
] as const
|
||||
|
||||
export const APPROVED_BASELINE_MENU_KEYS = NAV_ITEMS.map((item) => item.key)
|
||||
export const APPROVED_BASELINE_MENU_KEYS = [...NAV_ITEMS]
|
||||
.sort((a, b) => {
|
||||
if (a.key === 'dashboard') return -1
|
||||
if (b.key === 'dashboard') return 1
|
||||
return a.key.localeCompare(b.key)
|
||||
})
|
||||
.map((item) => item.key)
|
||||
|
||||
const ICON_MAP = {
|
||||
LayoutDashboard,
|
||||
@@ -152,6 +158,24 @@ export function getEmployeeLogoutRedirectUrl() {
|
||||
return websiteUrl
|
||||
}
|
||||
|
||||
function sortSidebarMenuItemsAlphabetically(
|
||||
items: GeneratedMenuItem[],
|
||||
resolveLabel: (item: GeneratedMenuItem) => string,
|
||||
): GeneratedMenuItem[] {
|
||||
return [...items]
|
||||
.map((item) => ({
|
||||
...item,
|
||||
children: sortSidebarMenuItemsAlphabetically(item.children, resolveLabel),
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (a.systemKey === 'dashboard') return -1
|
||||
if (b.systemKey === 'dashboard') return 1
|
||||
const labelCompare = resolveLabel(a).localeCompare(resolveLabel(b), undefined, { sensitivity: 'base' })
|
||||
if (labelCompare !== 0) return labelCompare
|
||||
return (a.systemKey ?? a.id).localeCompare(b.systemKey ?? b.id)
|
||||
})
|
||||
}
|
||||
|
||||
function notifyParent(message: Record<string, unknown>) {
|
||||
if (typeof window === 'undefined' || window.parent === window) return
|
||||
window.parent.postMessage(message, '*')
|
||||
@@ -172,6 +196,7 @@ export default function Sidebar() {
|
||||
const [menuItems, setMenuItems] = useState<GeneratedMenuItem[] | null>(null)
|
||||
const [menuAccessLevel, setMenuAccessLevel] = useState<SubscriptionAccessLevel | null>(null)
|
||||
const [menuLoadState, setMenuLoadState] = useState<'loading' | 'loaded' | 'failed'>('loading')
|
||||
const [unreadCount, setUnreadCount] = useState(0)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
@@ -276,6 +301,36 @@ export default function Sidebar() {
|
||||
// Close sidebar when navigating on mobile
|
||||
useEffect(() => { setOpen(false) }, [pathname])
|
||||
|
||||
async function refreshUnreadCount() {
|
||||
try {
|
||||
const data = await apiFetch<{ unread: number }>('/notifications/unread-count')
|
||||
setUnreadCount(data.unread)
|
||||
} catch {
|
||||
setUnreadCount(0)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refreshUnreadCount()
|
||||
}, [pathname])
|
||||
|
||||
useEffect(() => {
|
||||
const refresh = () => { void refreshUnreadCount() }
|
||||
const refreshWhenVisible = () => {
|
||||
if (document.visibilityState === 'visible') refresh()
|
||||
}
|
||||
window.addEventListener('notifications:updated', refresh)
|
||||
window.addEventListener('online', refresh)
|
||||
window.addEventListener('focus', refresh)
|
||||
document.addEventListener('visibilitychange', refreshWhenVisible)
|
||||
return () => {
|
||||
window.removeEventListener('notifications:updated', refresh)
|
||||
window.removeEventListener('online', refresh)
|
||||
window.removeEventListener('focus', refresh)
|
||||
document.removeEventListener('visibilitychange', refreshWhenVisible)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const isActive = (item: typeof NAV_ITEMS[number]) => {
|
||||
if ('exact' in item && item.exact) return appPath === item.href
|
||||
return appPath.startsWith(item.href)
|
||||
@@ -328,6 +383,10 @@ export default function Sidebar() {
|
||||
...generatedMenuItems,
|
||||
...ownerSystemMenuItems.filter((item) => !generatedRoutes.has(toDashboardAppPath(item.routeOrUrl))),
|
||||
]
|
||||
const sortedResolvedMenuItems = sortSidebarMenuItemsAlphabetically(
|
||||
resolvedMenuItems,
|
||||
(item) => item.systemKey ? (dict.nav[item.systemKey] ?? item.label) : item.label,
|
||||
)
|
||||
|
||||
function renderGeneratedMenu(items: GeneratedMenuItem[], depth = 0): ReactNode {
|
||||
return items.map((item) => {
|
||||
@@ -360,6 +419,9 @@ export default function Sidebar() {
|
||||
}
|
||||
|
||||
const active = mounted && isGeneratedItemActive(item)
|
||||
const badge = item.systemKey === 'notifications' && unreadCount > 0
|
||||
? unreadCount > 99 ? '99+' : String(unreadCount)
|
||||
: null
|
||||
const className = [
|
||||
'relative flex items-center gap-3 rounded-lg px-3.5 py-3 text-sm font-medium transition-all duration-200',
|
||||
paddingClass,
|
||||
@@ -388,7 +450,12 @@ export default function Sidebar() {
|
||||
return (
|
||||
<Link key={item.id} href={href} className={className}>
|
||||
{Icon ? <Icon className="h-4 w-4 flex-shrink-0" /> : null}
|
||||
{label}
|
||||
<span className="min-w-0 flex-1 truncate">{label}</span>
|
||||
{badge ? (
|
||||
<span className="ml-auto flex h-5 min-w-[20px] items-center justify-center rounded-full bg-orange-500 px-1.5 text-[11px] font-bold leading-none text-white">
|
||||
{badge}
|
||||
</span>
|
||||
) : null}
|
||||
</Link>
|
||||
)
|
||||
})
|
||||
@@ -454,7 +521,7 @@ export default function Sidebar() {
|
||||
</a>
|
||||
|
||||
<nav className="flex-1 space-y-1.5 overflow-y-auto px-3 py-5">
|
||||
{renderGeneratedMenu(resolvedMenuItems)}
|
||||
{renderGeneratedMenu(sortedResolvedMenuItems)}
|
||||
</nav>
|
||||
|
||||
<div className="border-t border-blue-200/70 px-3 py-4 dark:border-blue-400/10">
|
||||
|
||||
Reference in New Issue
Block a user