419 lines
18 KiB
TypeScript
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;
|