fix: enforce expired trial subscription menu access
Build & Deploy / Build & Push Docker Image (push) Successful in 2m48s
Test / Type Check (all packages) (push) Successful in 52s
Build & Deploy / Deploy to VPS (push) Successful in 4s
Test / API Unit Tests (push) Failing after 1m9s
Test / Homepage Unit Tests (push) Successful in 43s
Test / Storefront Unit Tests (push) Successful in 42s
Test / Admin Unit Tests (push) Successful in 47s
Test / Dashboard Unit Tests (push) Successful in 41s
Test / API Integration Tests (push) Successful in 1m0s
Build & Deploy / Build & Push Docker Image (push) Successful in 2m48s
Test / Type Check (all packages) (push) Successful in 52s
Build & Deploy / Deploy to VPS (push) Successful in 4s
Test / API Unit Tests (push) Failing after 1m9s
Test / Homepage Unit Tests (push) Successful in 43s
Test / Storefront Unit Tests (push) Successful in 42s
Test / Admin Unit Tests (push) Successful in 47s
Test / Dashboard Unit Tests (push) Successful in 41s
Test / API Integration Tests (push) Successful in 1m0s
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { APPROVED_BASELINE_MENU_KEYS, hasRenderableMenuItems } from './Sidebar'
|
||||
import { APPROVED_BASELINE_MENU_KEYS, hasRenderableMenuItems, shouldUseGeneratedMenu } from './Sidebar'
|
||||
|
||||
describe('Sidebar menu rendering helpers', () => {
|
||||
it('keeps the safe fallback menu to the approved seven baseline items in order', () => {
|
||||
@@ -17,6 +17,9 @@ describe('Sidebar menu rendering helpers', () => {
|
||||
it('treats an empty employee menu as non-renderable so fallback navigation remains available', () => {
|
||||
expect(hasRenderableMenuItems(null)).toBe(false)
|
||||
expect(hasRenderableMenuItems([])).toBe(false)
|
||||
expect(shouldUseGeneratedMenu('loaded', [])).toBe(false)
|
||||
expect(shouldUseGeneratedMenu('loaded', [], 'full')).toBe(false)
|
||||
expect(shouldUseGeneratedMenu('loaded', [], 'none')).toBe(true)
|
||||
})
|
||||
|
||||
it('ignores structural-only menu responses without clickable entries', () => {
|
||||
@@ -49,12 +52,12 @@ describe('Sidebar menu rendering helpers', () => {
|
||||
})
|
||||
|
||||
it('detects nested clickable menu entries', () => {
|
||||
expect(hasRenderableMenuItems([
|
||||
const menuItems = [
|
||||
{
|
||||
id: 'parent',
|
||||
systemKey: null,
|
||||
label: 'Operations',
|
||||
itemType: 'PARENT_MENU',
|
||||
itemType: 'PARENT_MENU' as const,
|
||||
routeOrUrl: null,
|
||||
icon: null,
|
||||
parentId: null,
|
||||
@@ -65,7 +68,7 @@ describe('Sidebar menu rendering helpers', () => {
|
||||
id: 'fleet',
|
||||
systemKey: 'fleet',
|
||||
label: 'Fleet',
|
||||
itemType: 'INTERNAL_PAGE',
|
||||
itemType: 'INTERNAL_PAGE' as const,
|
||||
routeOrUrl: '/fleet',
|
||||
icon: 'Car',
|
||||
parentId: 'parent',
|
||||
@@ -75,6 +78,10 @@ describe('Sidebar menu rendering helpers', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
])).toBe(true)
|
||||
]
|
||||
|
||||
expect(hasRenderableMenuItems(menuItems)).toBe(true)
|
||||
expect(shouldUseGeneratedMenu('loaded', menuItems)).toBe(true)
|
||||
expect(shouldUseGeneratedMenu('failed', menuItems)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -68,6 +68,8 @@ interface GeneratedMenuItem {
|
||||
children: GeneratedMenuItem[]
|
||||
}
|
||||
|
||||
type SubscriptionAccessLevel = 'full' | 'limited' | 'read_only' | 'none'
|
||||
|
||||
function computeInitials(name: string): string {
|
||||
const parts = name.trim().split(/\s+/).filter(Boolean)
|
||||
if (parts.length === 0) return 'U'
|
||||
@@ -136,6 +138,16 @@ export function hasRenderableMenuItems(items: GeneratedMenuItem[] | null): boole
|
||||
})
|
||||
}
|
||||
|
||||
export function shouldUseGeneratedMenu(
|
||||
menuLoadState: 'loading' | 'loaded' | 'failed',
|
||||
items: GeneratedMenuItem[] | null,
|
||||
subscriptionAccessLevel: SubscriptionAccessLevel | null = null,
|
||||
): boolean {
|
||||
if (menuLoadState !== 'loaded') return false
|
||||
if (subscriptionAccessLevel === 'none') return true
|
||||
return hasRenderableMenuItems(items)
|
||||
}
|
||||
|
||||
function notifyParent(message: Record<string, unknown>) {
|
||||
if (typeof window === 'undefined' || window.parent === window) return
|
||||
window.parent.postMessage(message, '*')
|
||||
@@ -154,6 +166,7 @@ export default function Sidebar() {
|
||||
})
|
||||
const [role, setRole] = useState<string>('AGENT')
|
||||
const [menuItems, setMenuItems] = useState<GeneratedMenuItem[] | null>(null)
|
||||
const [menuAccessLevel, setMenuAccessLevel] = useState<SubscriptionAccessLevel | null>(null)
|
||||
const [menuLoadState, setMenuLoadState] = useState<'loading' | 'loaded' | 'failed'>('loading')
|
||||
const [open, setOpen] = useState(false)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
@@ -221,7 +234,7 @@ export default function Sidebar() {
|
||||
|
||||
Promise.all([
|
||||
apiFetch<{ employee: EmployeeProfile }>('/auth/employee/me'),
|
||||
apiFetch<{ items: GeneratedMenuItem[] }>('/auth/employee/menu').catch(() => null),
|
||||
apiFetch<{ items: GeneratedMenuItem[]; subscriptionAccessLevel?: SubscriptionAccessLevel }>('/auth/employee/menu').catch(() => null),
|
||||
])
|
||||
.then(([{ employee }, menu]) => {
|
||||
if (cancelled) return
|
||||
@@ -230,8 +243,10 @@ export default function Sidebar() {
|
||||
if (employee.role) setRole(employee.role)
|
||||
if (menu) {
|
||||
setMenuItems(menu.items)
|
||||
setMenuAccessLevel(menu.subscriptionAccessLevel ?? null)
|
||||
setMenuLoadState('loaded')
|
||||
} else {
|
||||
setMenuAccessLevel(null)
|
||||
setMenuLoadState('failed')
|
||||
}
|
||||
if (employee.preferredLanguage === 'en' || employee.preferredLanguage === 'fr' || employee.preferredLanguage === 'ar') {
|
||||
@@ -247,6 +262,7 @@ export default function Sidebar() {
|
||||
.catch(() => {
|
||||
if (cancelled) return
|
||||
if (!cached) setUser(toSidebarUser({}, dict.workspaceUser, dict.localAuth))
|
||||
setMenuAccessLevel(null)
|
||||
setMenuLoadState('failed')
|
||||
})
|
||||
|
||||
@@ -283,7 +299,7 @@ export default function Sidebar() {
|
||||
children: [],
|
||||
}))
|
||||
|
||||
const useGeneratedMenu = menuLoadState === 'loaded'
|
||||
const useGeneratedMenu = shouldUseGeneratedMenu(menuLoadState, menuItems, menuAccessLevel)
|
||||
const ownerSystemMenuItems = OWNER_SYSTEM_NAV_ITEMS
|
||||
.filter((item) => !mounted || hasMinRole(role, item.minRole))
|
||||
.map((item) => ({
|
||||
|
||||
Reference in New Issue
Block a user