UNPKG

@pagamio/frontend-commons-lib

Version:

Pagamio library for Frontend reusable components like the form engine and table container

239 lines (238 loc) 9.36 kB
'use client'; import { jsx as _jsx } from "react/jsx-runtime"; import { HiChartPie } from 'react-icons/hi'; import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react'; import { isUUIDorID } from '../shared'; const AppBreadcrumbContext = createContext(undefined); /** * Create a flat map of all valid paths from the sidebar pages config */ function createPathMap(pages, parentPath = '') { const pathMap = new Map(); for (const page of pages) { const fullPath = parentPath + (page.href || ''); if (page.href) { pathMap.set(fullPath, { label: page.label, icon: page.icon }); } if (page.items) { const childPaths = createPathMap(page.items, fullPath); childPaths.forEach((value, key) => pathMap.set(key, value)); } } return pathMap; } /** * Check if a path represents a valid route by examining URL structure patterns * This is a conservative approach - only make something clickable if we're confident it exists */ function isValidRoute(segments, currentIndex, pathMap) { // Never make the last segment clickable (it's the current page) if (currentIndex === segments.length - 1) { return false; } // Build the path up to this segment const pathUpToHere = '/' + segments.slice(0, currentIndex + 1).join('/'); // If this exact path exists in the sidebar config, it's definitely clickable if (pathMap.has(pathUpToHere)) { return true; } const segment = segments[currentIndex]; // For UUID segments, only make clickable if there are non-UUID segments after // This means it's likely a detail page with sub-sections if (isUUIDorID(segment)) { for (let i = currentIndex + 1; i < segments.length; i++) { if (!isUUIDorID(segments[i])) { return true; } } return false; } // For regular segments (like "merchants", "vendors"), be very conservative // Only make clickable if it's in the sidebar config return false; } /** * Get appropriate label for a segment */ function getSegmentLabel(segment, parentPath, pathMap) { // Check if this exact path exists in sidebar const fullPath = parentPath + '/' + segment; const pageConfig = pathMap.get(fullPath); if (pageConfig) { return pageConfig.label; } // For UUID/ID segments, default to "Loading..." (will be updated by components) if (isUUIDorID(segment)) { return 'Loading...'; } // For regular segments, convert to readable format immediately // Handle common patterns like "sub-merchants" -> "Sub Merchants" return segment .split('-') .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } /** * Build breadcrumbs with proper route validation (Conservative mode - for VAS app) */ function buildBreadcrumbsFromPath(pathname, pathMap, manualUpdates) { const segments = pathname.replace(/\/$/, '').split('/').filter(Boolean); const breadcrumbs = []; let currentPath = ''; let rootIcon = undefined; for (let i = 0; i < segments.length; i++) { const segment = segments[i]; currentPath += `/${segment}`; const parentPath = '/' + segments.slice(0, i).join('/'); // Check if this exact path exists in sidebar config const pageConfig = pathMap.get(currentPath); // Determine if this should be clickable using improved logic const isClickable = isValidRoute(segments, i, pathMap); // Get the appropriate label let label; if (pageConfig) { // Use sidebar config label if available label = manualUpdates[currentPath] || pageConfig.label; } else if (isUUIDorID(segment)) { // For UUID segments, use manual update or default to "Loading..." label = manualUpdates[segment] || 'Loading...'; } else { // For regular segments, convert to readable format immediately label = manualUpdates[currentPath] || getSegmentLabel(segment, parentPath, pathMap); } // Set root icon from first exact match if (pageConfig && i === 0 && !rootIcon) { rootIcon = pageConfig.icon; } breadcrumbs.push({ label, path: isClickable ? currentPath : undefined, key: isUUIDorID(segment) ? segment : undefined, }); } return { breadcrumbs, rootIcon }; } /** * Build breadcrumbs for permissive mode */ function buildBreadcrumbsPermissive(pathname, pathMap, manualUpdates, pages) { const pathSegments = pathname.replace(/\/$/, '').split('/').filter(Boolean); let newBreadcrumbs = []; let icon = undefined; if (pathSegments.length === 0) { return { breadcrumbs: [{ label: 'Dashboard', path: '/dashboard' }], rootIcon: HiChartPie, }; } let currentPath = ''; // Find the deepest matching page config let foundConfig = undefined; function findPageConfigWithParents(pages, path, parents = []) { for (const page of pages) { if (page.href === path) return { page, parents }; if (page.items) { const found = findPageConfigWithParents(page.items, path, [...parents, page]); if (found.page) return found; } } return { page: undefined, parents: [] }; } for (const segment of pathSegments) { currentPath += `/${segment}`; const { page } = findPageConfigWithParents(pages, currentPath); if (page) { foundConfig = page; } } // Helper to set icon only if not already set const setIcon = (candidate) => { if (!icon && candidate) icon = candidate; }; function getManualLabel(segment, fallback) { return manualUpdates[segment] ?? fallback; } function getUUIDBreadcrumb(segment, currentPath, newBreadcrumbs) { const previousLabel = newBreadcrumbs[newBreadcrumbs.length - 1]?.label; if (previousLabel) { const baseLabel = previousLabel.replace(/s$/, ''); const label = getManualLabel(segment, `${baseLabel} Details`); return { label, path: currentPath, key: segment, }; } return null; } // Add the current page only (not parents) if (foundConfig) { const currentPath = foundConfig.href ?? ''; setIcon(foundConfig.icon); newBreadcrumbs.push({ label: getManualLabel(foundConfig.href ?? '', foundConfig.label), path: currentPath }); } // If there are extra segments (e.g. UUIDs), process them const extraSegments = pathSegments.slice(foundConfig?.href?.split('/').filter(Boolean).length ?? 0); let buildPath = foundConfig?.href ?? ''; for (const segment of extraSegments) { buildPath += `/${segment}`; if (isUUIDorID(segment)) { const uuidBreadcrumb = getUUIDBreadcrumb(segment, buildPath, newBreadcrumbs); if (uuidBreadcrumb) newBreadcrumbs.push(uuidBreadcrumb); } else { newBreadcrumbs.push({ label: getManualLabel(segment, segment.charAt(0).toUpperCase() + segment.slice(1)), path: buildPath, }); } } return { breadcrumbs: newBreadcrumbs, rootIcon: icon ?? HiChartPie }; } const AppBreadcrumbProvider = ({ children, pathname, pages, mode = 'conservative' }) => { const manualUpdates = useRef({}); const [forceUpdate, setForceUpdate] = useState(0); const pathMap = useMemo(() => createPathMap(pages), [pages]); const { breadcrumbs, rootPageIcon } = useMemo(() => { if (pathname === '/' || pathname === '') { return { breadcrumbs: [{ label: 'Dashboard', path: '/dashboard' }], rootPageIcon: HiChartPie, }; } if (mode === 'permissive') { const { breadcrumbs, rootIcon } = buildBreadcrumbsPermissive(pathname, pathMap, manualUpdates.current, pages); return { breadcrumbs, rootPageIcon: rootIcon ?? HiChartPie }; } else { const { breadcrumbs, rootIcon } = buildBreadcrumbsFromPath(pathname, pathMap, manualUpdates.current); return { breadcrumbs, rootPageIcon: rootIcon ?? HiChartPie }; } }, [pathname, pathMap, forceUpdate, mode, pages]); const updateBreadcrumb = useCallback((key, newLabel) => { manualUpdates.current[key] = newLabel; setForceUpdate((prev) => prev + 1); // Force re-render }, []); const value = useMemo(() => ({ breadcrumbs, updateBreadcrumb, pathname, rootPageIcon, }), [breadcrumbs, updateBreadcrumb, pathname, rootPageIcon]); return _jsx(AppBreadcrumbContext.Provider, { value: value, children: children }); }; const useAppBreadcrumbs = () => { const context = useContext(AppBreadcrumbContext); if (!context) { throw new Error('useBreadcrumbs must be used within a BreadcrumbProvider'); } return context; }; export default AppBreadcrumbProvider; export { useAppBreadcrumbs };