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:
chamikaJ
2025-06-25 17:08:40 +05:30
parent 3c7cacc46f
commit 44527f68cf
6 changed files with 144 additions and 120 deletions

View File

@@ -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}

View File

@@ -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" />

View File

@@ -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 (