- Replaced direct imports from 'antd' with centralized imports from '@/shared/antd-imports' to align with new import rules and improve maintainability.
403 lines
11 KiB
TypeScript
403 lines
11 KiB
TypeScript
import React, { lazy, Suspense, ComponentType, ReactNode } from 'react';
|
|
import { Skeleton, Spin } from '@/shared/antd-imports';
|
|
|
|
// Enhanced lazy loading with error boundary and retry logic
|
|
export function createOptimizedLazy<T extends ComponentType<any>>(
|
|
importFunc: () => Promise<{ default: T }>,
|
|
fallback?: ReactNode
|
|
): React.LazyExoticComponent<T> {
|
|
let retryCount = 0;
|
|
const maxRetries = 3;
|
|
|
|
const retryImport = async (): Promise<{ default: T }> => {
|
|
try {
|
|
return await importFunc();
|
|
} catch (error) {
|
|
if (retryCount < maxRetries) {
|
|
retryCount++;
|
|
console.warn(`Lazy loading failed, retrying... (${retryCount}/${maxRetries})`);
|
|
// Exponential backoff: 500ms, 1s, 2s
|
|
await new Promise(resolve => setTimeout(resolve, 500 * Math.pow(2, retryCount - 1)));
|
|
return retryImport();
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
return lazy(retryImport);
|
|
}
|
|
|
|
// Preloading utility for components that will likely be used
|
|
export const preloadComponent = <T extends ComponentType<any>>(
|
|
importFunc: () => Promise<{ default: T }>
|
|
): void => {
|
|
// Preload on user interaction or after a delay
|
|
if ('requestIdleCallback' in window) {
|
|
(window as any).requestIdleCallback(() => {
|
|
importFunc().catch(() => {
|
|
// Ignore preload errors
|
|
});
|
|
});
|
|
} else {
|
|
setTimeout(() => {
|
|
importFunc().catch(() => {
|
|
// Ignore preload errors
|
|
});
|
|
}, 2000);
|
|
}
|
|
};
|
|
|
|
// Lazy-loaded task management components with optimized fallbacks
|
|
export const LazyTaskListBoard = createOptimizedLazy(
|
|
() => import('./task-list-board'),
|
|
<div className="h-96 flex items-center justify-center">
|
|
<Spin size="large" tip="Loading task board..." />
|
|
</div>
|
|
);
|
|
|
|
export const LazyVirtualizedTaskList = createOptimizedLazy(
|
|
() => import('./virtualized-task-list'),
|
|
<Skeleton active paragraph={{ rows: 8 }} />
|
|
);
|
|
|
|
export const LazyTaskRow = createOptimizedLazy(
|
|
() => import('./task-row'),
|
|
<Skeleton.Input active style={{ width: '100%', height: 40 }} />
|
|
);
|
|
|
|
export const LazyImprovedTaskFilters = createOptimizedLazy(
|
|
() => import('./improved-task-filters'),
|
|
<Skeleton.Button active style={{ width: '100%', height: 60 }} />
|
|
);
|
|
|
|
export const LazyOptimizedBulkActionBar = createOptimizedLazy(
|
|
() => import('./optimized-bulk-action-bar'),
|
|
<Skeleton.Button active style={{ width: '100%', height: 48 }} />
|
|
);
|
|
|
|
export const LazyPerformanceAnalysis = createOptimizedLazy(
|
|
() => import('./performance-analysis'),
|
|
<div className="p-4 text-center">Loading performance tools...</div>
|
|
);
|
|
|
|
// Kanban-specific components
|
|
export const LazyKanbanTaskListBoard = createOptimizedLazy(
|
|
() => import('../kanban-board-management-v2/kanbanTaskListBoard'),
|
|
<div className="h-96 flex items-center justify-center">
|
|
<Spin size="large" tip="Loading kanban board..." />
|
|
</div>
|
|
);
|
|
|
|
// Task list V2 components
|
|
export const LazyTaskListV2Table = createOptimizedLazy(
|
|
() => import('../task-list-v2/TaskListV2Table'),
|
|
<Skeleton active paragraph={{ rows: 10 }} />
|
|
);
|
|
|
|
export const LazyTaskRowWithSubtasks = createOptimizedLazy(
|
|
() => import('../task-list-v2/TaskRowWithSubtasks'),
|
|
<Skeleton.Input active style={{ width: '100%', height: 40 }} />
|
|
);
|
|
|
|
export const LazyCustomColumnModal = createOptimizedLazy(
|
|
() => import('@/pages/projects/projectView/taskList/task-list-table/custom-columns/custom-column-modal/custom-column-modal'),
|
|
<div className="p-4"><Skeleton active /></div>
|
|
);
|
|
|
|
export const LazyLabelsSelector = createOptimizedLazy(
|
|
() => import('@/components/LabelsSelector'),
|
|
<Skeleton.Button active style={{ width: 120, height: 24 }} />
|
|
);
|
|
|
|
export const LazyAssigneeSelector = createOptimizedLazy(
|
|
() => import('./lazy-assignee-selector'),
|
|
<Skeleton.Avatar active size="small" />
|
|
);
|
|
|
|
export const LazyTaskStatusDropdown = createOptimizedLazy(
|
|
() => import('./task-status-dropdown'),
|
|
<Skeleton.Button active style={{ width: 100, height: 24 }} />
|
|
);
|
|
|
|
export const LazyTaskPriorityDropdown = createOptimizedLazy(
|
|
() => import('./task-priority-dropdown'),
|
|
<Skeleton.Button active style={{ width: 80, height: 24 }} />
|
|
);
|
|
|
|
export const LazyTaskPhaseDropdown = createOptimizedLazy(
|
|
() => import('./task-phase-dropdown'),
|
|
<Skeleton.Button active style={{ width: 90, height: 24 }} />
|
|
);
|
|
|
|
// HOC for progressive enhancement
|
|
interface ProgressiveEnhancementProps {
|
|
condition: boolean;
|
|
children: ReactNode;
|
|
fallback?: ReactNode;
|
|
loadingComponent?: ReactNode;
|
|
}
|
|
|
|
export const ProgressiveEnhancement: React.FC<ProgressiveEnhancementProps> = ({
|
|
condition,
|
|
children,
|
|
fallback,
|
|
loadingComponent = <Skeleton active />
|
|
}) => {
|
|
if (!condition) {
|
|
return <>{fallback || loadingComponent}</>;
|
|
}
|
|
|
|
return (
|
|
<Suspense fallback={loadingComponent}>
|
|
{children}
|
|
</Suspense>
|
|
);
|
|
};
|
|
|
|
// Intersection observer based lazy loading for components
|
|
interface IntersectionLazyLoadProps {
|
|
children: ReactNode;
|
|
fallback?: ReactNode;
|
|
rootMargin?: string;
|
|
threshold?: number;
|
|
once?: boolean;
|
|
}
|
|
|
|
export const IntersectionLazyLoad: React.FC<IntersectionLazyLoadProps> = ({
|
|
children,
|
|
fallback = <Skeleton active />,
|
|
rootMargin = '100px',
|
|
threshold = 0.1,
|
|
once = true
|
|
}) => {
|
|
const [isVisible, setIsVisible] = React.useState(false);
|
|
const [hasBeenVisible, setHasBeenVisible] = React.useState(false);
|
|
const ref = React.useRef<HTMLDivElement>(null);
|
|
|
|
React.useEffect(() => {
|
|
const element = ref.current;
|
|
if (!element) return;
|
|
|
|
const observer = new IntersectionObserver(
|
|
([entry]) => {
|
|
if (entry.isIntersecting) {
|
|
setIsVisible(true);
|
|
if (once) {
|
|
setHasBeenVisible(true);
|
|
observer.disconnect();
|
|
}
|
|
} else if (!once) {
|
|
setIsVisible(false);
|
|
}
|
|
},
|
|
{ rootMargin, threshold }
|
|
);
|
|
|
|
observer.observe(element);
|
|
|
|
return () => {
|
|
observer.disconnect();
|
|
};
|
|
}, [rootMargin, threshold, once]);
|
|
|
|
const shouldRender = isVisible || hasBeenVisible;
|
|
|
|
return (
|
|
<div ref={ref}>
|
|
{shouldRender ? (
|
|
<Suspense fallback={fallback}>
|
|
{children}
|
|
</Suspense>
|
|
) : (
|
|
fallback
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Route-based code splitting utility
|
|
export const createRouteComponent = <T extends ComponentType<any>>(
|
|
importFunc: () => Promise<{ default: T }>,
|
|
pageTitle?: string
|
|
) => {
|
|
const LazyComponent = createOptimizedLazy(importFunc);
|
|
|
|
return React.memo(() => {
|
|
React.useEffect(() => {
|
|
if (pageTitle) {
|
|
document.title = pageTitle;
|
|
}
|
|
}, []);
|
|
|
|
return (
|
|
<Suspense
|
|
fallback={
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<Spin size="large" tip={`Loading ${pageTitle || 'page'}...`} />
|
|
</div>
|
|
}
|
|
>
|
|
<LazyComponent {...({} as any)} />
|
|
</Suspense>
|
|
);
|
|
});
|
|
};
|
|
|
|
// Bundle splitting by feature
|
|
export const TaskManagementBundle = {
|
|
TaskListBoard: LazyTaskListBoard,
|
|
VirtualizedTaskList: LazyVirtualizedTaskList,
|
|
TaskRow: LazyTaskRow,
|
|
TaskFilters: LazyImprovedTaskFilters,
|
|
BulkActionBar: LazyOptimizedBulkActionBar,
|
|
PerformanceAnalysis: LazyPerformanceAnalysis,
|
|
};
|
|
|
|
export const KanbanBundle = {
|
|
KanbanBoard: LazyKanbanTaskListBoard,
|
|
};
|
|
|
|
export const TaskListV2Bundle = {
|
|
TaskTable: LazyTaskListV2Table,
|
|
TaskRowWithSubtasks: LazyTaskRowWithSubtasks,
|
|
};
|
|
|
|
export const FormBundle = {
|
|
CustomColumnModal: LazyCustomColumnModal,
|
|
LabelsSelector: LazyLabelsSelector,
|
|
AssigneeSelector: LazyAssigneeSelector,
|
|
};
|
|
|
|
export const DropdownBundle = {
|
|
StatusDropdown: LazyTaskStatusDropdown,
|
|
PriorityDropdown: LazyTaskPriorityDropdown,
|
|
PhaseDropdown: LazyTaskPhaseDropdown,
|
|
};
|
|
|
|
// Preloading strategies
|
|
export const preloadTaskManagementComponents = () => {
|
|
// Preload core components that are likely to be used
|
|
preloadComponent(() => import('./task-list-board'));
|
|
preloadComponent(() => import('./virtualized-task-list'));
|
|
preloadComponent(() => import('./improved-task-filters'));
|
|
};
|
|
|
|
export const preloadKanbanComponents = () => {
|
|
preloadComponent(() => import('../kanban-board-management-v2/kanbanTaskListBoard'));
|
|
};
|
|
|
|
export const preloadFormComponents = () => {
|
|
preloadComponent(() => import('@/components/LabelsSelector'));
|
|
};
|
|
|
|
// Dynamic import utilities
|
|
export const importTaskComponent = async (componentName: string) => {
|
|
const componentMap: Record<string, () => Promise<any>> = {
|
|
'task-list-board': () => import('./task-list-board'),
|
|
'virtualized-task-list': () => import('./virtualized-task-list'),
|
|
'task-row': () => import('./task-row'),
|
|
'improved-task-filters': () => import('./improved-task-filters'),
|
|
'optimized-bulk-action-bar': () => import('./optimized-bulk-action-bar'),
|
|
'performance-analysis': () => import('./performance-analysis'),
|
|
};
|
|
|
|
const importFunc = componentMap[componentName];
|
|
if (!importFunc) {
|
|
throw new Error(`Component ${componentName} not found`);
|
|
}
|
|
|
|
return importFunc();
|
|
};
|
|
|
|
// Error boundary for lazy loaded components
|
|
interface LazyErrorBoundaryState {
|
|
hasError: boolean;
|
|
error?: Error;
|
|
}
|
|
|
|
export class LazyErrorBoundary extends React.Component<
|
|
{ children: ReactNode; fallback?: ReactNode },
|
|
LazyErrorBoundaryState
|
|
> {
|
|
constructor(props: { children: ReactNode; fallback?: ReactNode }) {
|
|
super(props);
|
|
this.state = { hasError: false };
|
|
}
|
|
|
|
static getDerivedStateFromError(error: Error): LazyErrorBoundaryState {
|
|
return { hasError: true, error };
|
|
}
|
|
|
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
console.error('Lazy loading error:', error, errorInfo);
|
|
}
|
|
|
|
render() {
|
|
if (this.state.hasError) {
|
|
return (
|
|
this.props.fallback || (
|
|
<div className="p-4 text-center text-red-600">
|
|
<p>Failed to load component</p>
|
|
<button
|
|
onClick={() => window.location.reload()}
|
|
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
|
>
|
|
Reload Page
|
|
</button>
|
|
</div>
|
|
)
|
|
);
|
|
}
|
|
|
|
return this.props.children;
|
|
}
|
|
}
|
|
|
|
// Usage example and documentation
|
|
export const LazyLoadingExamples = {
|
|
// Basic lazy loading with suspense
|
|
BasicExample: () => (
|
|
<Suspense fallback={<Skeleton active />}>
|
|
<LazyTaskListBoard projectId="123" />
|
|
</Suspense>
|
|
),
|
|
|
|
// Progressive enhancement
|
|
ProgressiveExample: () => (
|
|
<ProgressiveEnhancement condition={true}>
|
|
<LazyTaskRow
|
|
task={{ id: '1', status: 'todo', priority: 'medium', created_at: '', updated_at: '' }}
|
|
projectId="123"
|
|
groupId="group1"
|
|
currentGrouping="status"
|
|
isSelected={false}
|
|
onSelect={() => {}}
|
|
onToggleSubtasks={() => {}}
|
|
/>
|
|
</ProgressiveEnhancement>
|
|
),
|
|
|
|
// Intersection observer lazy loading
|
|
IntersectionExample: () => (
|
|
<IntersectionLazyLoad rootMargin="200px">
|
|
<LazyPerformanceAnalysis projectId="123" />
|
|
</IntersectionLazyLoad>
|
|
),
|
|
|
|
// Error boundary with lazy loading
|
|
ErrorBoundaryExample: () => (
|
|
<LazyErrorBoundary>
|
|
<Suspense fallback={<Skeleton active />}>
|
|
<LazyTaskRow
|
|
task={{ id: '1', status: 'todo', priority: 'medium', created_at: '', updated_at: '' }}
|
|
projectId="123"
|
|
groupId="group1"
|
|
currentGrouping="status"
|
|
isSelected={false}
|
|
onSelect={() => {}}
|
|
onToggleSubtasks={() => {}}
|
|
/>
|
|
</Suspense>
|
|
</LazyErrorBoundary>
|
|
),
|
|
};
|