ui-nyla/src/core/PageManager/SidebarProvider.tsx
2026-01-12 13:33:51 +01:00

419 lines
18 KiB
TypeScript

import React, { createContext, useContext, useState, useEffect } from 'react';
import { allPageData, SidebarItem } from './data';
import { useLanguage } from '../../providers/language/LanguageContext';
import { resolveLanguageText } from './pageInterface';
import { usePermissions } from '../../hooks/usePermissions';
import { getUserDataCache } from '../../utils/userCache';
import { FaHome, FaHatWizard } from 'react-icons/fa';
import { RiFolderSettingsFill } from 'react-icons/ri';
// Configuration for parent groups that don't have a page definition
// Maps parentPath to icon and default order
const parentGroupConfig: Record<string, {
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
defaultOrder?: number;
}> = {
'start': {
icon: FaHome,
defaultOrder: 1
},
'administration': {
icon: RiFolderSettingsFill,
defaultOrder: 2
},
'admin': {
icon: FaHatWizard,
defaultOrder: 3
}
};
interface SidebarContextType {
sidebarItems: SidebarItem[];
loading: boolean;
error: string | null;
refreshSidebar: () => Promise<void>;
}
const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
export const useSidebar = () => {
const context = useContext(SidebarContext);
if (!context) {
throw new Error('useSidebar must be used within a SidebarProvider');
}
return context;
};
interface SidebarProviderProps {
children: React.ReactNode;
}
export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) => {
const [sidebarItems, setSidebarItems] = useState<SidebarItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Get translation function from language context
const { t } = useLanguage();
const { canView } = usePermissions();
// Get sidebar items from page data
const getSidebarItems = async (): Promise<SidebarItem[]> => {
const items: SidebarItem[] = [];
// Get all unique parent paths from pages that have subpages
const parentPaths = new Set<string>();
allPageData.forEach(page => {
if (page.parentPath && !page.hide && page.showInSidebar !== false) {
parentPaths.add(page.parentPath);
}
});
// Create parent groups for each parentPath (even if no page exists for that path)
const parentGroups = new Map<string, {
id: string;
name: string;
icon?: React.ComponentType<React.SVGProps<SVGSVGElement>>;
order: number;
subpages: typeof allPageData;
}>();
for (const parentPath of parentPaths) {
// Check if a page exists for this parent path
const parentPage = allPageData.find(p => p.path === parentPath && !p.hide);
// Get all subpages for this parent
const subpages = allPageData.filter(p =>
p.parentPath === parentPath &&
!p.hide &&
p.showInSidebar !== false
);
if (subpages.length > 0) {
// Use parent page data if it exists, otherwise create a virtual parent
// Try to resolve name from translation key (e.g., "start.title") or use capitalized path
let parentName: string;
if (parentPage) {
parentName = resolveLanguageText(parentPage.name, t);
} else {
// Try to resolve as translation key first (e.g., "start.title")
const translationKey = `${parentPath}.title`;
const translated = t(translationKey);
parentName = translated !== translationKey ? translated : parentPath.charAt(0).toUpperCase() + parentPath.slice(1);
}
// Get icon: use parent page icon if exists, otherwise use config, or undefined
const parentIcon = parentPage?.icon || parentGroupConfig[parentPath]?.icon;
// Determine order: use parent page order if exists, otherwise use config default,
// then minimum order of subpages, or default to 0
let parentOrder = parentPage?.order;
if (parentOrder === undefined) {
parentOrder = parentGroupConfig[parentPath]?.defaultOrder;
if (parentOrder === undefined) {
const subpageOrders = subpages.map(s => s.order ?? 0);
parentOrder = subpageOrders.length > 0 ? Math.min(...subpageOrders) : 0;
}
}
parentGroups.set(parentPath, {
id: parentPage?.id || parentPath,
name: parentName,
icon: parentIcon,
order: parentOrder,
subpages: subpages
});
}
}
// Process parent groups
for (const [_parentPath, parentGroup] of parentGroups.entries()) {
// Filter subpages by RBAC access and privilegeChecker
const accessibleSubpages = [];
for (const subpage of parentGroup.subpages) {
try {
// Check RBAC access
const hasSubpageRBACAccess = await canView('UI', subpage.path);
if (!hasSubpageRBACAccess) {
continue;
}
// Check client-side privilegeChecker if provided
if (subpage.privilegeChecker) {
try {
const hasPrivilege = await subpage.privilegeChecker();
if (!hasPrivilege) {
continue;
}
} catch (error) {
console.error(`Error checking privilegeChecker for subpage ${subpage.path}:`, error);
continue;
}
}
accessibleSubpages.push(subpage);
} catch (error) {
console.error(`Error checking RBAC access for subpage ${subpage.path}:`, error);
}
}
if (accessibleSubpages.length > 0) {
// Create parent item with submenu (no link since it's not a real page)
items.push({
id: parentGroup.id,
name: parentGroup.name,
link: undefined, // No link - parent is not a clickable page
icon: parentGroup.icon,
moduleEnabled: true,
order: parentGroup.order,
submenu: accessibleSubpages.map(subpage => ({
id: subpage.id,
name: resolveLanguageText(subpage.name, t),
link: `/${subpage.path}`,
icon: subpage.icon
}))
});
}
}
// Get main pages (no parent path)
const mainPages = allPageData
.filter(page => !page.parentPath && !page.hide && page.showInSidebar !== false)
.sort((a, b) => (a.order || 0) - (b.order || 0));
// Log user info for debugging
const cachedUser = getUserDataCache();
console.log('👤 SidebarProvider: Current user info:', {
username: cachedUser?.username,
roleLabels: cachedUser?.roleLabels,
roleLabelsLength: Array.isArray(cachedUser?.roleLabels) ? cachedUser.roleLabels.length : 0
});
// Process each main page
console.log('📋 SidebarProvider: Processing pages, total:', mainPages.length, 'pages to check');
const pageAccessResults: Array<{ path: string; name: string; hasAccess: boolean }> = [];
for (const pageData of mainPages) {
console.log('🔍 SidebarProvider: Checking access for page:', {
path: pageData.path,
name: pageData.name,
hasSubpages: pageData.hasSubpages
});
// Check RBAC permissions
try {
const hasRBACAccess = await canView('UI', pageData.path);
console.log('🔍 SidebarProvider: RBAC check result:', {
path: pageData.path,
hasAccess: hasRBACAccess
});
if (!hasRBACAccess) {
console.log('⛔ SidebarProvider: Page hidden due to RBAC:', pageData.path);
continue;
}
// Check client-side privilegeChecker if provided
if (pageData.privilegeChecker) {
try {
const hasPrivilege = await pageData.privilegeChecker();
if (!hasPrivilege) {
console.log('⛔ SidebarProvider: Page hidden due to privilegeChecker:', pageData.path);
continue;
}
} catch (error) {
console.error(`❌ SidebarProvider: Error checking privilegeChecker for ${pageData.path}:`, error);
continue;
}
}
} catch (error) {
console.error(`❌ SidebarProvider: Error checking RBAC access for ${pageData.path}:`, error);
continue;
}
// Check if this page has subpages
if (pageData.hasSubpages) {
// Find all subpages for this parent
const allSubpages = allPageData.filter(p =>
p.parentPath === pageData.path &&
!p.hide &&
p.showInSidebar !== false
);
// Filter subpages by RBAC access
const accessibleSubpages = [];
console.log('📋 SidebarProvider: Checking subpages for:', {
parentPath: pageData.path,
totalSubpages: allSubpages.length
});
for (const subpage of allSubpages) {
try {
console.log('🔍 SidebarProvider: Checking subpage access:', {
parentPath: pageData.path,
subpagePath: subpage.path,
subpageName: subpage.name
});
const hasSubpageRBACAccess = await canView('UI', subpage.path);
console.log('🔍 SidebarProvider: Subpage RBAC result:', {
subpagePath: subpage.path,
hasAccess: hasSubpageRBACAccess
});
if (!hasSubpageRBACAccess) {
console.log('⛔ SidebarProvider: Subpage hidden due to RBAC:', subpage.path);
continue;
}
// Check client-side privilegeChecker if provided
if (subpage.privilegeChecker) {
try {
const hasPrivilege = await subpage.privilegeChecker();
if (!hasPrivilege) {
console.log('⛔ SidebarProvider: Subpage hidden due to privilegeChecker:', subpage.path);
continue;
}
} catch (error) {
console.error(`❌ SidebarProvider: Error checking privilegeChecker for subpage ${subpage.path}:`, error);
continue;
}
}
accessibleSubpages.push(subpage);
console.log('✅ SidebarProvider: Subpage added:', subpage.path);
} catch (error) {
console.error(`❌ SidebarProvider: Error checking RBAC access for subpage ${subpage.path}:`, error);
}
}
console.log('📋 SidebarProvider: Subpage filtering complete:', {
parentPath: pageData.path,
totalSubpages: allSubpages.length,
accessibleSubpages: accessibleSubpages.length,
accessiblePaths: accessibleSubpages.map(s => s.path)
});
if (accessibleSubpages.length > 0) {
console.log('✅ SidebarProvider: Adding parent page with subpages:', {
path: pageData.path,
name: pageData.name,
subpagesCount: accessibleSubpages.length
});
// Create expandable item with submenu
items.push({
id: pageData.id,
name: resolveLanguageText(pageData.name, t),
link: `/${pageData.path}`,
icon: pageData.icon,
moduleEnabled: pageData.moduleEnabled ?? true,
order: pageData.order || 0,
submenu: accessibleSubpages.map(subpage => ({
id: subpage.id,
name: resolveLanguageText(subpage.name, t),
link: `/${subpage.path}`,
icon: subpage.icon
}))
});
} else {
// No accessible subpages, show as regular item
console.log('✅ SidebarProvider: Adding parent page without accessible subpages:', {
path: pageData.path,
name: pageData.name
});
items.push({
id: pageData.id,
name: resolveLanguageText(pageData.name, t),
link: `/${pageData.path}`,
icon: pageData.icon,
moduleEnabled: pageData.moduleEnabled ?? true,
order: pageData.order || 0
});
}
} else {
// Regular items without subpages
console.log('✅ SidebarProvider: Adding regular page:', {
path: pageData.path,
name: pageData.name
});
items.push({
id: pageData.id,
name: resolveLanguageText(pageData.name, t),
link: `/${pageData.path}`,
icon: pageData.icon,
moduleEnabled: pageData.moduleEnabled ?? true,
order: pageData.order || 0
});
}
}
// Sort all items by order
const sortedItems = items.sort((a, b) => (a.order || 0) - (b.order || 0));
// Summary of page access checks
const accessiblePages = pageAccessResults.filter(r => r.hasAccess);
const deniedPages = pageAccessResults.filter(r => !r.hasAccess);
console.log('📊 SidebarProvider: Page access summary:', {
totalPagesChecked: pageAccessResults.length,
accessiblePages: accessiblePages.length,
deniedPages: deniedPages.length,
accessiblePagePaths: accessiblePages.map(p => p.path),
deniedPagePaths: deniedPages.map(p => p.path),
deniedPageDetails: deniedPages.map(p => ({ path: p.path, name: p.name }))
});
console.log('📊 SidebarProvider: Final sidebar items built and sorted:', {
totalItems: sortedItems.length,
sortedPaths: sortedItems.map(item => item.link),
items: sortedItems.map(item => ({
id: item.id,
link: item.link,
name: item.name,
hasSubmenu: !!item.submenu,
submenuCount: item.submenu?.length || 0
}))
});
return sortedItems;
};
// Refresh sidebar items
const refreshSidebar = async () => {
console.log('🔄 SidebarProvider: Refreshing sidebar items...');
setLoading(true);
setError(null);
try {
const items = await getSidebarItems();
console.log('✅ SidebarProvider: Setting sidebar items:', {
count: items.length,
items: items.map(item => ({ id: item.id, link: item.link, name: item.name }))
});
setSidebarItems(items);
} catch (err) {
console.error('❌ SidebarProvider: Error refreshing sidebar:', err);
setError(err instanceof Error ? err.message : 'Failed to load sidebar items');
} finally {
setLoading(false);
}
};
// Load sidebar items on mount and when language changes
useEffect(() => {
refreshSidebar();
}, [t]);
const contextValue: SidebarContextType = {
sidebarItems,
loading,
error,
refreshSidebar
};
return (
<SidebarContext.Provider value={contextValue}>
{children}
</SidebarContext.Provider>
);
};
export default SidebarProvider;