feat(assignee-selector, suspense-fallback, project-view): optimize component loading and enhance user experience
- Integrated synchronous imports for TaskListFilters and filter dropdowns to improve performance and reduce loading times. - Refactored AssigneeSelector to include a new invite member drawer functionality, enhancing user interaction. - Simplified SuspenseFallback components for better loading experiences, ensuring they do not block the main UI. - Updated project view constants to utilize InlineSuspenseFallback for lazy-loaded components, improving rendering efficiency.
This commit is contained in:
@@ -10,6 +10,8 @@ import { SocketEvents } from '@/shared/socket-events';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import { Avatar, Button, Checkbox } from '@/components';
|
||||
import { sortTeamMembers } from '@/utils/sort-team-members';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { toggleProjectMemberDrawer } from '@/features/projects/singleProject/members/projectMembersSlice';
|
||||
|
||||
interface AssigneeSelectorProps {
|
||||
task: IProjectTask;
|
||||
@@ -34,6 +36,7 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
||||
const members = useSelector((state: RootState) => state.teamMembersReducer.teamMembers);
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
const { socket } = useSocket();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const filteredMembers = useMemo(() => {
|
||||
return teamMembers?.data?.filter(member =>
|
||||
@@ -149,6 +152,11 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
||||
return assignees?.includes(memberId) || false;
|
||||
};
|
||||
|
||||
const handleInviteProjectMemberDrawer = () => {
|
||||
setIsOpen(false); // Close the assignee dropdown first
|
||||
dispatch(toggleProjectMemberDrawer()); // Then open the invite drawer
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
@@ -271,10 +279,7 @@ const AssigneeSelector: React.FC<AssigneeSelectorProps> = ({
|
||||
: 'text-blue-600 hover:bg-blue-50'
|
||||
}
|
||||
`}
|
||||
onClick={() => {
|
||||
// TODO: Implement invite member functionality
|
||||
console.log('Invite member clicked');
|
||||
}}
|
||||
onClick={handleInviteProjectMemberDrawer}
|
||||
>
|
||||
<UserAddOutlined />
|
||||
Invite member
|
||||
|
||||
@@ -1,50 +1,42 @@
|
||||
import React, { memo } from 'react';
|
||||
import { colors } from '@/styles/colors';
|
||||
import { getInitialTheme } from '@/utils/get-initial-theme';
|
||||
import { ConfigProvider, theme, Layout, Spin } from 'antd';
|
||||
import { Spin } from 'antd';
|
||||
|
||||
// Memoized loading component with theme awareness
|
||||
// Lightweight loading component - removed heavy theme calculations
|
||||
export const SuspenseFallback = memo(() => {
|
||||
const currentTheme = getInitialTheme();
|
||||
const isDark = currentTheme === 'dark';
|
||||
|
||||
// Memoize theme configuration to prevent unnecessary re-renders
|
||||
const themeConfig = React.useMemo(() => ({
|
||||
algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
||||
components: {
|
||||
Layout: {
|
||||
colorBgLayout: isDark ? colors.darkGray : '#fafafa',
|
||||
},
|
||||
Spin: {
|
||||
colorPrimary: isDark ? '#fff' : '#1890ff',
|
||||
},
|
||||
},
|
||||
}), [isDark]);
|
||||
|
||||
// Memoize layout style to prevent object recreation
|
||||
const layoutStyle = React.useMemo(() => ({
|
||||
position: 'fixed' as const,
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
background: 'transparent',
|
||||
transition: 'none',
|
||||
}), []);
|
||||
|
||||
// Memoize spin style to prevent object recreation
|
||||
const spinStyle = React.useMemo(() => ({
|
||||
position: 'absolute' as const,
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}), []);
|
||||
|
||||
return (
|
||||
<ConfigProvider theme={themeConfig}>
|
||||
<Layout className="app-loading-container" style={layoutStyle}>
|
||||
<Spin size="large" style={spinStyle} />
|
||||
</Layout>
|
||||
</ConfigProvider>
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'transparent',
|
||||
zIndex: 9999,
|
||||
}}
|
||||
>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// Lightweight fallback for internal components that doesn't cover the screen
|
||||
export const InlineSuspenseFallback = memo(() => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '40px 20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: '200px',
|
||||
}}
|
||||
>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
SuspenseFallback.displayName = 'SuspenseFallback';
|
||||
InlineSuspenseFallback.displayName = 'InlineSuspenseFallback';
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import React, { ReactNode, Suspense } from 'react';
|
||||
import { InlineSuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
|
||||
|
||||
// Lazy load all project view components to reduce initial bundle size
|
||||
// Import core components synchronously to avoid suspense in main tabs
|
||||
import ProjectViewEnhancedTasks from '@/pages/projects/projectView/enhancedTasks/project-view-enhanced-tasks';
|
||||
import ProjectViewEnhancedBoard from '@/pages/projects/projectView/enhancedBoard/project-view-enhanced-board';
|
||||
|
||||
// Lazy load less critical components
|
||||
const ProjectViewInsights = React.lazy(() => import('@/pages/projects/projectView/insights/project-view-insights'));
|
||||
const ProjectViewFiles = React.lazy(() => import('@/pages/projects/projectView/files/project-view-files'));
|
||||
const ProjectViewMembers = React.lazy(() => import('@/pages/projects/projectView/members/project-view-members'));
|
||||
const ProjectViewUpdates = React.lazy(() => import('@/pages/projects/project-view-1/updates/project-view-updates'));
|
||||
const ProjectViewEnhancedTasks = React.lazy(() => import('@/pages/projects/projectView/enhancedTasks/project-view-enhanced-tasks'));
|
||||
const ProjectViewEnhancedBoard = React.lazy(() => import('@/pages/projects/projectView/enhancedBoard/project-view-enhanced-board'));
|
||||
|
||||
// type of a tab items
|
||||
type TabItems = {
|
||||
@@ -37,24 +40,36 @@ export const tabItems: TabItems[] = [
|
||||
index: 2,
|
||||
key: 'project-insights-member-overview',
|
||||
label: 'Insights',
|
||||
element: React.createElement(ProjectViewInsights),
|
||||
element: React.createElement(Suspense,
|
||||
{ fallback: React.createElement(InlineSuspenseFallback) },
|
||||
React.createElement(ProjectViewInsights)
|
||||
),
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
key: 'all-attachments',
|
||||
label: 'Files',
|
||||
element: React.createElement(ProjectViewFiles),
|
||||
element: React.createElement(Suspense,
|
||||
{ fallback: React.createElement(InlineSuspenseFallback) },
|
||||
React.createElement(ProjectViewFiles)
|
||||
),
|
||||
},
|
||||
{
|
||||
index: 4,
|
||||
key: 'members',
|
||||
label: 'Members',
|
||||
element: React.createElement(ProjectViewMembers),
|
||||
element: React.createElement(Suspense,
|
||||
{ fallback: React.createElement(InlineSuspenseFallback) },
|
||||
React.createElement(ProjectViewMembers)
|
||||
),
|
||||
},
|
||||
{
|
||||
index: 5,
|
||||
key: 'updates',
|
||||
label: 'Updates',
|
||||
element: React.createElement(ProjectViewUpdates),
|
||||
element: React.createElement(Suspense,
|
||||
{ fallback: React.createElement(InlineSuspenseFallback) },
|
||||
React.createElement(ProjectViewUpdates)
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState, useRef, useMemo, useCallback, lazy, Suspense } from 'react';
|
||||
import { useEffect, useState, useRef, useMemo, useCallback } from 'react';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
|
||||
const TaskListFilters = lazy(() => import('../taskList/task-list-filters/task-list-filters'));
|
||||
import TaskListFilters from '../taskList/task-list-filters/task-list-filters';
|
||||
import { Flex, Skeleton } from 'antd';
|
||||
import BoardSectionCardContainer from './board-section/board-section-container';
|
||||
import {
|
||||
@@ -44,6 +44,7 @@ import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status'
|
||||
import { debounce } from 'lodash';
|
||||
import { ITaskListPriorityChangeResponse } from '@/types/tasks/task-list-priority.types';
|
||||
import { updateTaskPriority as updateBoardTaskPriority } from '@/features/board/board-slice';
|
||||
|
||||
interface DroppableContainer {
|
||||
id: UniqueIdentifier;
|
||||
data: {
|
||||
@@ -554,9 +555,7 @@ const ProjectViewBoard = () => {
|
||||
|
||||
return (
|
||||
<Flex vertical gap={16}>
|
||||
<Suspense fallback={<div>Loading filters...</div>}>
|
||||
<TaskListFilters position={'board'} />
|
||||
</Suspense>
|
||||
<TaskListFilters position={'board'} />
|
||||
<Skeleton active loading={isLoading} className='mt-4 p-4'>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useState, useMemo, lazy, Suspense } from 'react';
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import { Empty } from '@/shared/antd-imports';
|
||||
import Flex from 'antd/es/flex';
|
||||
import Skeleton from 'antd/es/skeleton';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
const TaskListFilters = lazy(() => import('./task-list-filters/task-list-filters'));
|
||||
import TaskListFilters from './task-list-filters/task-list-filters';
|
||||
import TaskGroupWrapperOptimized from './task-group-wrapper-optimized';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
@@ -17,7 +17,7 @@ const ProjectViewTaskList = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { projectView } = useTabSearchParam();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
|
||||
const [coreDataLoaded, setCoreDataLoaded] = useState(false);
|
||||
|
||||
// Split selectors to prevent unnecessary rerenders
|
||||
const projectId = useAppSelector(state => state.projectReducer.projectId);
|
||||
@@ -33,11 +33,11 @@ const ProjectViewTaskList = () => {
|
||||
|
||||
const loadingPhases = useAppSelector(state => state.phaseReducer.loadingPhases);
|
||||
|
||||
// Single source of truth for loading state - EXCLUDE labels loading from skeleton
|
||||
// Labels loading should not block the main task list display
|
||||
// Simplified loading state - only wait for essential data
|
||||
// Remove dependency on phases and status categories for initial render
|
||||
const isLoading = useMemo(() =>
|
||||
loadingGroups || loadingPhases || loadingStatusCategories || !initialLoadComplete,
|
||||
[loadingGroups, loadingPhases, loadingStatusCategories, initialLoadComplete]
|
||||
loadingGroups || !coreDataLoaded,
|
||||
[loadingGroups, coreDataLoaded]
|
||||
);
|
||||
|
||||
// Memoize the empty state check
|
||||
@@ -56,53 +56,63 @@ const ProjectViewTaskList = () => {
|
||||
}
|
||||
}, [projectView, setSearchParams, searchParams]);
|
||||
|
||||
// Batch initial data fetching - core data only
|
||||
// Optimized parallel data fetching - don't wait for everything
|
||||
useEffect(() => {
|
||||
const fetchInitialData = async () => {
|
||||
if (!projectId || !groupBy || initialLoadComplete) return;
|
||||
const fetchCoreData = async () => {
|
||||
if (!projectId || !groupBy || coreDataLoaded) return;
|
||||
|
||||
try {
|
||||
// Batch only essential API calls for initial load
|
||||
// Filter data (labels, assignees, etc.) will load separately and not block the UI
|
||||
await Promise.allSettled([
|
||||
// Start all requests in parallel, but only wait for task columns
|
||||
// Other data can load in background without blocking UI
|
||||
const corePromises = [
|
||||
dispatch(fetchTaskListColumns(projectId)),
|
||||
dispatch(fetchPhasesByProjectId(projectId)),
|
||||
dispatch(fetchStatusesCategories()),
|
||||
]);
|
||||
setInitialLoadComplete(true);
|
||||
dispatch(fetchTaskGroups(projectId)), // Start immediately
|
||||
];
|
||||
|
||||
// Background data - don't wait for these
|
||||
dispatch(fetchPhasesByProjectId(projectId));
|
||||
dispatch(fetchStatusesCategories());
|
||||
|
||||
// Only wait for essential data
|
||||
await Promise.allSettled(corePromises);
|
||||
setCoreDataLoaded(true);
|
||||
} catch (error) {
|
||||
console.error('Error fetching initial data:', error);
|
||||
setInitialLoadComplete(true); // Still mark as complete to prevent infinite loading
|
||||
console.error('Error fetching core data:', error);
|
||||
setCoreDataLoaded(true); // Still mark as complete to prevent infinite loading
|
||||
}
|
||||
};
|
||||
|
||||
fetchInitialData();
|
||||
}, [projectId, groupBy, dispatch, initialLoadComplete]);
|
||||
fetchCoreData();
|
||||
}, [projectId, groupBy, dispatch, coreDataLoaded]);
|
||||
|
||||
// Fetch task groups with dependency on initial load completion
|
||||
// Optimized task groups fetching - remove initialLoadComplete dependency
|
||||
useEffect(() => {
|
||||
const fetchTasks = async () => {
|
||||
if (!projectId || !groupBy || projectView !== 'list' || !initialLoadComplete) return;
|
||||
if (!projectId || !groupBy || projectView !== 'list') return;
|
||||
|
||||
try {
|
||||
await dispatch(fetchTaskGroups(projectId));
|
||||
// Only refetch if filters change, not on initial load
|
||||
if (coreDataLoaded) {
|
||||
await dispatch(fetchTaskGroups(projectId));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching task groups:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTasks();
|
||||
}, [projectId, groupBy, projectView, dispatch, fields, search, archived, initialLoadComplete]);
|
||||
// Only refetch when filters change
|
||||
if (coreDataLoaded) {
|
||||
fetchTasks();
|
||||
}
|
||||
}, [projectId, groupBy, projectView, dispatch, fields, search, archived, coreDataLoaded]);
|
||||
|
||||
// Memoize the task groups to prevent unnecessary re-renders
|
||||
const memoizedTaskGroups = useMemo(() => taskGroups || [], [taskGroups]);
|
||||
|
||||
return (
|
||||
<Flex vertical gap={16} style={{ overflowX: 'hidden' }}>
|
||||
{/* Filters load independently and don't block the main content */}
|
||||
<Suspense fallback={<div>Loading filters...</div>}>
|
||||
<TaskListFilters position="list" />
|
||||
</Suspense>
|
||||
{/* Filters load synchronously - no suspense boundary */}
|
||||
<TaskListFilters position="list" />
|
||||
|
||||
{isEmptyState ? (
|
||||
<Empty description="No tasks group found" />
|
||||
|
||||
@@ -16,13 +16,14 @@ import {
|
||||
import { getTeamMembers } from '@/features/team-members/team-members.slice';
|
||||
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
||||
|
||||
const SearchDropdown = React.lazy(() => import('@components/project-task-filters/filter-dropdowns/search-dropdown'));
|
||||
const SortFilterDropdown = React.lazy(() => import('@components/project-task-filters/filter-dropdowns/sort-filter-dropdown'));
|
||||
const LabelsFilterDropdown = React.lazy(() => import('@components/project-task-filters/filter-dropdowns/labels-filter-dropdown'));
|
||||
const MembersFilterDropdown = React.lazy(() => import('@components/project-task-filters/filter-dropdowns/members-filter-dropdown'));
|
||||
const GroupByFilterDropdown = React.lazy(() => import('@components/project-task-filters/filter-dropdowns/group-by-filter-dropdown'));
|
||||
const ShowFieldsFilterDropdown = React.lazy(() => import('@components/project-task-filters/filter-dropdowns/show-fields-filter-dropdown'));
|
||||
const PriorityFilterDropdown = React.lazy(() => import('@components/project-task-filters/filter-dropdowns/priority-filter-dropdown'));
|
||||
// Import filter components synchronously for better performance
|
||||
import SearchDropdown from '@components/project-task-filters/filter-dropdowns/search-dropdown';
|
||||
import SortFilterDropdown from '@components/project-task-filters/filter-dropdowns/sort-filter-dropdown';
|
||||
import LabelsFilterDropdown from '@components/project-task-filters/filter-dropdowns/labels-filter-dropdown';
|
||||
import MembersFilterDropdown from '@components/project-task-filters/filter-dropdowns/members-filter-dropdown';
|
||||
import GroupByFilterDropdown from '@components/project-task-filters/filter-dropdowns/group-by-filter-dropdown';
|
||||
import ShowFieldsFilterDropdown from '@components/project-task-filters/filter-dropdowns/show-fields-filter-dropdown';
|
||||
import PriorityFilterDropdown from '@components/project-task-filters/filter-dropdowns/priority-filter-dropdown';
|
||||
|
||||
interface TaskListFiltersProps {
|
||||
position: 'board' | 'list';
|
||||
@@ -39,44 +40,46 @@ const TaskListFilters: React.FC<TaskListFiltersProps> = ({ position }) => {
|
||||
|
||||
const handleShowArchivedChange = () => dispatch(toggleArchived());
|
||||
|
||||
// Load filter data asynchronously and non-blocking
|
||||
// This runs independently of the main task list loading
|
||||
// Optimized filter data loading - staggered and non-blocking
|
||||
useEffect(() => {
|
||||
const loadFilterData = async () => {
|
||||
const loadFilterData = () => {
|
||||
try {
|
||||
// Load priorities first (usually cached/fast)
|
||||
// Load priorities first (usually cached/fast) - immediate
|
||||
if (!priorities.length) {
|
||||
dispatch(fetchPriorities());
|
||||
}
|
||||
|
||||
// Load project-specific filter data in parallel, but don't await
|
||||
// This allows the main task list to load while filters are still loading
|
||||
if (projectId) {
|
||||
// Fire and forget - these will update the UI when ready
|
||||
dispatch(fetchLabelsByProject(projectId));
|
||||
dispatch(fetchTaskAssignees(projectId));
|
||||
}
|
||||
// Stagger the loading to prevent overwhelming the server
|
||||
// Load project-specific data with delays
|
||||
setTimeout(() => {
|
||||
dispatch(fetchLabelsByProject(projectId));
|
||||
}, 100);
|
||||
|
||||
// Load team members (usually needed for member filters)
|
||||
dispatch(getTeamMembers({
|
||||
index: 0,
|
||||
size: 100,
|
||||
field: null,
|
||||
order: null,
|
||||
search: null,
|
||||
all: true
|
||||
}));
|
||||
setTimeout(() => {
|
||||
dispatch(fetchTaskAssignees(projectId));
|
||||
}, 200);
|
||||
|
||||
// Load team members last (heaviest query)
|
||||
setTimeout(() => {
|
||||
dispatch(getTeamMembers({
|
||||
index: 0,
|
||||
size: 50, // Reduce initial load size
|
||||
field: null,
|
||||
order: null,
|
||||
search: null,
|
||||
all: false // Don't load all at once
|
||||
}));
|
||||
}, 300);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading filter data:', error);
|
||||
// Don't throw - filter loading errors shouldn't break the main UI
|
||||
}
|
||||
};
|
||||
|
||||
// Use setTimeout to ensure this runs after the main component render
|
||||
// This prevents filter loading from blocking the initial render
|
||||
const timeoutId = setTimeout(loadFilterData, 0);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
// Load immediately without setTimeout to avoid additional delay
|
||||
loadFilterData();
|
||||
}, [dispatch, priorities.length, projectId]);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user