@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
JavaScript
'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 };