feat(project-drawer): enhance project data fetching and error handling

- Updated project data fetching logic in the project drawer and related components to ensure the drawer opens only after successful data retrieval.
- Added detailed logging for successful and failed fetch attempts to improve debugging and user feedback.
- Introduced error handling to maintain user experience by allowing the drawer to open even if data fetching fails, displaying an error state.
- Refactored project list and project view components to optimize search functionality and improve loading states.
- Removed deprecated components related to task management to streamline the project view.
This commit is contained in:
chamikaJ
2025-07-07 17:07:45 +05:30
parent 978d9158c0
commit b0253135e5
12 changed files with 450 additions and 984 deletions

View File

@@ -2314,6 +2314,32 @@
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1"
}
},
"node_modules/@tanstack/query-core": {
"version": "5.81.5",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.81.5.tgz",
"integrity": "sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.81.5",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.81.5.tgz",
"integrity": "sha512-lOf2KqRRiYWpQT86eeeftAGnjuTR35myTP8MXyvHa81VlomoAWNEd8x5vkcAfQefu0qtYCvyqLropFZqgI2EQw==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.81.5"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^18 || ^19"
}
},
"node_modules/@tanstack/react-table": {
"version": "8.21.3",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",

View File

@@ -124,10 +124,25 @@ const ProjectGroupList: React.FC<ProjectGroupListProps> = ({
// Action handlers
const handleSettingsClick = (e: React.MouseEvent, projectId: string) => {
e.stopPropagation();
console.log('Opening project drawer from project group for project:', projectId);
trackMixpanelEvent(evt_projects_settings_click);
// Set project ID first
dispatch(setProjectId(projectId));
dispatch(fetchProjectData(projectId));
// Then fetch project data
dispatch(fetchProjectData(projectId))
.unwrap()
.then((projectData) => {
console.log('Project data fetched successfully from project group:', projectData);
// Open drawer after data is fetched
dispatch(toggleProjectDrawer());
})
.catch((error) => {
console.error('Failed to fetch project data from project group:', error);
// Still open drawer even if fetch fails, so user can see error state
dispatch(toggleProjectDrawer());
});
};
const handleArchiveClick = async (

View File

@@ -46,10 +46,25 @@ export const ActionButtons: React.FC<ActionButtonsProps> = ({
const handleSettingsClick = () => {
if (record.id) {
console.log('Opening project drawer for project:', record.id);
trackMixpanelEvent(evt_projects_settings_click);
// Set project ID first
dispatch(setProjectId(record.id));
dispatch(fetchProjectData(record.id));
// Then fetch project data
dispatch(fetchProjectData(record.id))
.unwrap()
.then((projectData) => {
console.log('Project data fetched successfully:', projectData);
// Open drawer after data is fetched
dispatch(toggleProjectDrawer());
})
.catch((error) => {
console.error('Failed to fetch project data:', error);
// Still open drawer even if fetch fails, so user can see error state
dispatch(toggleProjectDrawer());
});
}
};

View File

@@ -72,6 +72,7 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
null
);
const [isFormValid, setIsFormValid] = useState<boolean>(true);
const [drawerVisible, setDrawerVisible] = useState<boolean>(false);
// Selectors
const { clients, loading: loadingClients } = useAppSelector(state => state.clientReducer);
@@ -131,6 +132,60 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
loadInitialData();
}, [dispatch]);
// New effect to handle form population when project data becomes available
useEffect(() => {
if (drawerVisible && projectId && project && !projectLoading) {
console.log('Populating form with project data:', project);
setEditMode(true);
try {
form.setFieldsValue({
...project,
start_date: project.start_date ? dayjs(project.start_date) : null,
end_date: project.end_date ? dayjs(project.end_date) : null,
working_days: project.working_days || 0,
use_manual_progress: project.use_manual_progress || false,
use_weighted_progress: project.use_weighted_progress || false,
use_time_progress: project.use_time_progress || false,
});
setSelectedProjectManager(project.project_manager || null);
setLoading(false);
console.log('Form populated successfully with project data');
} catch (error) {
console.error('Error setting form values:', error);
logger.error('Error setting form values in project drawer', error);
setLoading(false);
}
} else if (drawerVisible && !projectId) {
// Creating new project
console.log('Setting up drawer for new project creation');
setEditMode(false);
setLoading(false);
} else if (drawerVisible && projectId && !project && !projectLoading) {
// Project data failed to load or is empty
console.warn('Project drawer is visible but no project data available');
setLoading(false);
} else if (drawerVisible && projectId) {
console.log('Drawer visible, waiting for project data to load...');
}
}, [drawerVisible, projectId, project, projectLoading, form]);
// Additional effect to handle loading state when project data is being fetched
useEffect(() => {
if (drawerVisible && projectId && projectLoading) {
console.log('Project data is loading, maintaining loading state');
setLoading(true);
}
}, [drawerVisible, projectId, projectLoading]);
// Define resetForm function early to avoid declaration order issues
const resetForm = useCallback(() => {
setEditMode(false);
form.resetFields();
setSelectedProjectManager(null);
}, [form]);
useEffect(() => {
const startDate = form.getFieldValue('start_date');
const endDate = form.getFieldValue('end_date');
@@ -226,47 +281,33 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
return workingDays;
};
// Improved handleVisibilityChange to track drawer state without doing form operations
const handleVisibilityChange = useCallback(
(visible: boolean) => {
if (visible && projectId) {
setEditMode(true);
if (project) {
form.setFieldsValue({
...project,
start_date: project.start_date ? dayjs(project.start_date) : null,
end_date: project.end_date ? dayjs(project.end_date) : null,
working_days:
form.getFieldValue('start_date') && form.getFieldValue('end_date')
? calculateWorkingDays(
form.getFieldValue('start_date'),
form.getFieldValue('end_date')
)
: project.working_days || 0,
use_manual_progress: project.use_manual_progress || false,
use_weighted_progress: project.use_weighted_progress || false,
use_time_progress: project.use_time_progress || false,
});
setSelectedProjectManager(project.project_manager || null);
setLoading(false);
}
} else {
console.log('Drawer visibility changed:', visible, 'Project ID:', projectId);
setDrawerVisible(visible);
if (!visible) {
resetForm();
} else if (visible && !projectId) {
// Creating new project - reset form immediately
console.log('Opening drawer for new project');
setEditMode(false);
setLoading(false);
} else if (visible && projectId) {
// Editing existing project - loading state will be handled by useEffect
console.log('Opening drawer for existing project:', projectId);
setLoading(true);
}
},
[projectId, project]
[projectId, resetForm]
);
const resetForm = useCallback(() => {
setEditMode(false);
form.resetFields();
setSelectedProjectManager(null);
}, [form]);
const handleDrawerClose = useCallback(() => {
setLoading(true);
setDrawerVisible(false);
resetForm();
dispatch(setProjectData({} as IProjectViewModel));
// dispatch(setProjectId(null));
dispatch(setDrawerProjectId(null));
dispatch(toggleProjectDrawer());
onClose();
@@ -405,7 +446,7 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => {
{!isEditable && (
<Alert message={t('noPermission')} type="warning" showIcon style={{ marginBottom: 16 }} />
)}
<Skeleton active paragraph={{ rows: 12 }} loading={projectLoading}>
<Skeleton active paragraph={{ rows: 12 }} loading={loading || projectLoading}>
<Form
form={form}
layout="vertical"

View File

@@ -20,10 +20,31 @@ export const fetchProjectData = createAsyncThunk(
'project/fetchProjectData',
async (projectId: string, { rejectWithValue, dispatch }) => {
try {
if (!projectId) {
throw new Error('Project ID is required');
}
console.log(`Fetching project data for ID: ${projectId}`);
const response = await projectsApiService.getProject(projectId);
if (!response) {
throw new Error('No response received from API');
}
if (!response.done) {
throw new Error(response.message || 'API request failed');
}
if (!response.body) {
throw new Error('No project data in response body');
}
console.log(`Successfully fetched project data:`, response.body);
return response.body;
} catch (error) {
return rejectWithValue(error instanceof Error ? error.message : 'Failed to fetch project');
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch project';
console.error(`Error fetching project data for ID ${projectId}:`, error);
return rejectWithValue(errorMessage);
}
}
);
@@ -44,16 +65,21 @@ const projectDrawerSlice = createSlice({
},
extraReducers: builder => {
builder
.addCase(fetchProjectData.pending, state => {
console.log('Starting project data fetch...');
state.projectLoading = true;
state.project = null; // Clear existing data while loading
})
.addCase(fetchProjectData.fulfilled, (state, action) => {
console.log('Project data fetch completed successfully:', action.payload);
state.project = action.payload;
state.projectLoading = false;
})
.addCase(fetchProjectData.rejected, state => {
.addCase(fetchProjectData.rejected, (state, action) => {
console.error('Project data fetch failed:', action.payload);
state.projectLoading = false;
state.project = null;
// You could add an error field to the state if needed for UI feedback
});
},
});

View File

@@ -2,7 +2,6 @@ import React, { ReactNode, Suspense } from 'react';
import { InlineSuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
// 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';
import TaskListV2 from '@/components/task-list-v2/TaskListV2';

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { ProjectViewType, ProjectGroupBy } from '@/types/project/project.types';
@@ -85,6 +85,10 @@ const createFilters = (items: { id: string; name: string }[]) =>
const ProjectList: React.FC = () => {
const [filteredInfo, setFilteredInfo] = useState<Record<string, FilterValue | null>>({});
const [isLoading, setIsLoading] = useState(false);
const [searchValue, setSearchValue] = useState('');
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const lastQueryParamsRef = useRef<string>('');
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const { t } = useTranslation('all-project-list');
const dispatch = useAppDispatch();
@@ -103,12 +107,130 @@ const ProjectList: React.FC = () => {
const { projectCategories } = useAppSelector(state => state.projectCategoriesReducer);
const { filteredCategories, filteredStatuses } = useAppSelector(state => state.projectsReducer);
// Optimize query parameters to prevent unnecessary re-renders
const optimizedQueryParams = useMemo(() => {
const params = {
index: requestParams.index,
size: requestParams.size,
field: requestParams.field,
order: requestParams.order,
search: requestParams.search,
filter: requestParams.filter,
statuses: requestParams.statuses,
categories: requestParams.categories,
};
// Create a stable key for comparison
const paramsKey = JSON.stringify(params);
// Only return new params if they've actually changed
if (paramsKey !== lastQueryParamsRef.current) {
lastQueryParamsRef.current = paramsKey;
return params;
}
return params;
}, [requestParams]);
// Use the optimized query with better error handling and caching
const {
data: projectsData,
isLoading: loadingProjects,
isFetching: isFetchingProjects,
refetch: refetchProjects,
} = useGetProjectsQuery(requestParams);
error: projectsError,
} = useGetProjectsQuery(optimizedQueryParams, {
// Enable caching and reduce unnecessary refetches
refetchOnMountOrArgChange: 30, // Refetch if data is older than 30 seconds
refetchOnFocus: false, // Don't refetch on window focus
refetchOnReconnect: true, // Refetch on network reconnect
// Skip query if we're in group view mode
skip: viewMode === ProjectViewType.GROUP,
});
// Add performance monitoring
const performanceRef = useRef<{ startTime: number | null }>({ startTime: null });
// Monitor query performance
useEffect(() => {
if (loadingProjects && !performanceRef.current.startTime) {
performanceRef.current.startTime = performance.now();
} else if (!loadingProjects && performanceRef.current.startTime) {
const duration = performance.now() - performanceRef.current.startTime;
console.log(`Projects query completed in ${duration.toFixed(2)}ms`);
performanceRef.current.startTime = null;
}
}, [loadingProjects]);
// Optimized debounced search with better cleanup and performance
const debouncedSearch = useCallback(
debounce((searchTerm: string) => {
console.log('Executing debounced search:', searchTerm);
// Clear any error messages when starting a new search
setErrorMessage(null);
if (viewMode === ProjectViewType.LIST) {
dispatch(setRequestParams({
search: searchTerm,
index: 1 // Reset to first page on search
}));
} else if (viewMode === ProjectViewType.GROUP) {
const newGroupedParams = {
...groupedRequestParams,
search: searchTerm,
index: 1,
};
dispatch(setGroupedRequestParams(newGroupedParams));
// Add timeout for grouped search to prevent rapid API calls
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
searchTimeoutRef.current = setTimeout(() => {
dispatch(fetchGroupedProjects(newGroupedParams));
}, 100);
}
}, 500), // Increased debounce time for better performance
[dispatch, viewMode, groupedRequestParams]
);
// Enhanced cleanup with better timeout management
useEffect(() => {
return () => {
debouncedSearch.cancel();
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
searchTimeoutRef.current = null;
}
};
}, [debouncedSearch]);
// Improved search change handler with better validation
const handleSearchChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const newSearchValue = e.target.value;
// Validate input length to prevent excessive API calls
if (newSearchValue.length > 100) {
return; // Prevent extremely long search terms
}
setSearchValue(newSearchValue);
trackMixpanelEvent(evt_projects_search);
// Clear any existing timeout
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
searchTimeoutRef.current = null;
}
// Debounce the actual search execution
debouncedSearch(newSearchValue);
},
[debouncedSearch, trackMixpanelEvent]
);
const getFilterIndex = useCallback(() => {
return +(localStorage.getItem(FILTER_INDEX_KEY) || 0);
@@ -247,14 +369,15 @@ const ProjectList: React.FC = () => {
// Memoize the table data source
const tableDataSource = useMemo(() => projectsData?.body?.data || [], [projectsData?.body?.data]);
// Memoize the empty text component
const emptyText = useMemo(() => <Empty description={t('noProjects')} />, [t]);
// Memoize the pagination show total function
const paginationShowTotal = useMemo(
() => (total: number, range: [number, number]) => `${range[0]}-${range[1]} of ${total} groups`,
[]
);
// Handle query errors
useEffect(() => {
if (projectsError) {
console.error('Projects query error:', projectsError);
setErrorMessage('Failed to load projects. Please try again.');
} else {
setErrorMessage(null);
}
}, [projectsError]);
const handleTableChange = useCallback(
(
@@ -262,135 +385,108 @@ const ProjectList: React.FC = () => {
filters: Record<string, FilterValue | null>,
sorter: SorterResult<IProjectViewModel> | SorterResult<IProjectViewModel>[]
) => {
const newParams: Partial<typeof requestParams> = {};
// Batch all parameter updates to reduce re-renders
const updates: Partial<typeof requestParams> = {};
let hasChanges = false;
// Handle status filters
if (filters?.status_id !== filteredInfo.status_id) {
if (!filters?.status_id) {
newParams.statuses = null;
updates.statuses = null;
dispatch(setFilteredStatuses([]));
} else {
newParams.statuses = filters.status_id.join(' ');
updates.statuses = filters.status_id.join(' ');
}
hasChanges = true;
}
// Handle category filters
if (filters?.category_id !== filteredInfo.category_id) {
if (!filters?.category_id) {
newParams.categories = null;
updates.categories = null;
dispatch(setFilteredCategories([]));
} else {
newParams.categories = filters.category_id.join(' ');
updates.categories = filters.category_id.join(' ');
}
hasChanges = true;
}
// Handle sorting
const newOrder = Array.isArray(sorter) ? sorter[0].order : sorter.order;
const newField = (Array.isArray(sorter) ? sorter[0].columnKey : sorter.columnKey) as string;
if (newOrder && newField) {
newParams.order = newOrder ?? 'ascend';
newParams.field = newField ?? 'name';
setSortingValues(newParams.field, newParams.order);
if (newOrder && newField && (newOrder !== requestParams.order || newField !== requestParams.field)) {
updates.order = newOrder ?? 'ascend';
updates.field = newField ?? 'name';
setSortingValues(updates.field, updates.order);
hasChanges = true;
}
newParams.index = newPagination.current || 1;
newParams.size = newPagination.pageSize || DEFAULT_PAGE_SIZE;
// Handle pagination
if (newPagination.current !== requestParams.index || newPagination.pageSize !== requestParams.size) {
updates.index = newPagination.current || 1;
updates.size = newPagination.pageSize || DEFAULT_PAGE_SIZE;
hasChanges = true;
}
dispatch(setRequestParams(newParams));
// Only dispatch if there are actual changes
if (hasChanges) {
dispatch(setRequestParams(updates));
// Also update grouped request params to keep them in sync
dispatch(
setGroupedRequestParams({
...groupedRequestParams,
statuses: newParams.statuses,
categories: newParams.categories,
order: newParams.order,
field: newParams.field,
index: newParams.index,
size: newParams.size,
...updates,
})
);
}
setFilteredInfo(filters);
},
[dispatch, setSortingValues, groupedRequestParams]
[dispatch, setSortingValues, groupedRequestParams, filteredInfo, requestParams]
);
// Optimized grouped table change handler
const handleGroupedTableChange = useCallback(
(newPagination: TablePaginationConfig) => {
const newParams: Partial<typeof groupedRequestParams> = {
index: newPagination.current || 1,
size: newPagination.pageSize || DEFAULT_PAGE_SIZE,
};
// Only update if values actually changed
if (newParams.index !== groupedRequestParams.index || newParams.size !== groupedRequestParams.size) {
dispatch(setGroupedRequestParams(newParams));
}
},
[dispatch, groupedRequestParams]
);
const handleRefresh = useCallback(() => {
trackMixpanelEvent(evt_projects_refresh_click);
if (viewMode === ProjectViewType.LIST) {
refetchProjects();
} else if (viewMode === ProjectViewType.GROUP && groupBy) {
dispatch(fetchGroupedProjects(groupedRequestParams));
}
}, [trackMixpanelEvent, refetchProjects, viewMode, groupBy, dispatch, groupedRequestParams]);
// Optimized segment change handler with better state management
const handleSegmentChange = useCallback(
(value: IProjectFilter) => {
const newFilterIndex = filters.indexOf(value);
setFilterIndex(newFilterIndex);
// Update both request params for consistency
dispatch(setRequestParams({ filter: newFilterIndex }));
dispatch(
setGroupedRequestParams({
...groupedRequestParams,
filter: newFilterIndex,
index: 1, // Reset to first page when changing filter
})
);
// Batch updates to reduce re-renders
const baseUpdates = { filter: newFilterIndex, index: 1 };
// Refresh data based on current view mode
if (viewMode === ProjectViewType.LIST) {
refetchProjects();
} else if (viewMode === ProjectViewType.GROUP && groupBy) {
dispatch(
fetchGroupedProjects({
dispatch(setRequestParams(baseUpdates));
dispatch(setGroupedRequestParams({
...groupedRequestParams,
filter: newFilterIndex,
index: 1,
})
);
...baseUpdates,
}));
// Only trigger data fetch for group view (list view will auto-refetch via query)
if (viewMode === ProjectViewType.GROUP && groupBy) {
dispatch(fetchGroupedProjects({
...groupedRequestParams,
...baseUpdates,
}));
}
},
[filters, setFilterIndex, dispatch, refetchProjects, viewMode, groupBy, groupedRequestParams]
);
// Debounced search for grouped projects
const debouncedGroupedSearch = useCallback(
debounce((params: typeof groupedRequestParams) => {
if (groupBy) {
dispatch(fetchGroupedProjects(params));
}
}, 300),
[dispatch, groupBy]
);
const handleSearchChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const searchValue = e.target.value;
trackMixpanelEvent(evt_projects_search);
// Update both request params for consistency
dispatch(setRequestParams({ search: searchValue, index: 1 }));
if (viewMode === ProjectViewType.GROUP) {
const newGroupedParams = {
...groupedRequestParams,
search: searchValue,
index: 1,
};
dispatch(setGroupedRequestParams(newGroupedParams));
// Trigger debounced search in group mode
debouncedGroupedSearch(newGroupedParams);
}
},
[dispatch, trackMixpanelEvent, viewMode, groupedRequestParams, debouncedGroupedSearch]
[filters, setFilterIndex, dispatch, groupedRequestParams, viewMode, groupBy]
);
const handleViewToggle = useCallback(
@@ -557,20 +653,19 @@ const ProjectList: React.FC = () => {
]
);
useEffect(() => {
if (viewMode === ProjectViewType.LIST) {
setIsLoading(loadingProjects || isFetchingProjects);
} else {
setIsLoading(groupedProjects.loading);
}
}, [loadingProjects, isFetchingProjects, viewMode, groupedProjects.loading]);
// Optimize useEffect hooks to reduce unnecessary API calls
useEffect(() => {
const filterIndex = getFilterIndex();
dispatch(setRequestParams({ filter: filterIndex }));
// Also sync with grouped request params on initial load
dispatch(
setGroupedRequestParams({
const initialParams = { filter: filterIndex };
// Only update if values are different
if (requestParams.filter !== filterIndex) {
dispatch(setRequestParams(initialParams));
}
// Initialize grouped request params only once
if (!groupedRequestParams.groupBy) {
dispatch(setGroupedRequestParams({
filter: filterIndex,
index: 1,
size: DEFAULT_PAGE_SIZE,
@@ -580,29 +675,69 @@ const ProjectList: React.FC = () => {
groupBy: '',
statuses: null,
categories: null,
})
);
}, [dispatch, getFilterIndex]);
}));
}
}, [dispatch, getFilterIndex]); // Remove requestParams and groupedRequestParams from deps to avoid loops
// Separate effect for tracking page visits - only run once
useEffect(() => {
trackMixpanelEvent(evt_projects_page_visit);
if (viewMode === ProjectViewType.LIST) {
refetchProjects();
}
}, [requestParams, refetchProjects, trackMixpanelEvent, viewMode]);
}, [trackMixpanelEvent]);
// Separate useEffect for grouped projects
// Optimized effect for grouped projects - only fetch when necessary
useEffect(() => {
if (viewMode === ProjectViewType.GROUP && groupBy) {
if (viewMode === ProjectViewType.GROUP && groupBy && groupedRequestParams.groupBy) {
dispatch(fetchGroupedProjects(groupedRequestParams));
}
}, [dispatch, viewMode, groupBy, groupedRequestParams]);
// Optimize lookups loading - only fetch once
useEffect(() => {
if (projectStatuses.length === 0) dispatch(fetchProjectStatuses());
if (projectCategories.length === 0) dispatch(fetchProjectCategories());
if (projectHealths.length === 0) dispatch(fetchProjectHealth());
}, [dispatch, projectStatuses.length, projectCategories.length, projectHealths.length]);
const loadLookups = async () => {
const promises = [];
if (projectStatuses.length === 0) {
promises.push(dispatch(fetchProjectStatuses()));
}
if (projectCategories.length === 0) {
promises.push(dispatch(fetchProjectCategories()));
}
if (projectHealths.length === 0) {
promises.push(dispatch(fetchProjectHealth()));
}
// Load all lookups in parallel
if (promises.length > 0) {
await Promise.allSettled(promises);
}
};
loadLookups();
}, [dispatch]); // Remove length dependencies to avoid re-runs
// Sync search input value with Redux state
useEffect(() => {
const currentSearch = viewMode === ProjectViewType.LIST ? requestParams.search : groupedRequestParams.search;
if (searchValue !== currentSearch) {
setSearchValue(currentSearch || '');
}
}, [requestParams.search, groupedRequestParams.search, viewMode, searchValue]);
// Optimize loading state management
useEffect(() => {
let newLoadingState = false;
if (viewMode === ProjectViewType.LIST) {
newLoadingState = loadingProjects || isFetchingProjects;
} else {
newLoadingState = groupedProjects.loading;
}
// Only update if loading state actually changed
if (isLoading !== newLoadingState) {
setIsLoading(newLoadingState);
}
}, [loadingProjects, isFetchingProjects, viewMode, groupedProjects.loading, isLoading]);
return (
<div style={{ marginBlock: 65, minHeight: '90vh' }}>
@@ -638,9 +773,14 @@ const ProjectList: React.FC = () => {
placeholder={t('placeholder')}
suffix={<SearchOutlined />}
type="text"
value={requestParams.search}
value={searchValue}
onChange={handleSearchChange}
aria-label="Search projects"
allowClear
onClear={() => {
setSearchValue('');
debouncedSearch('');
}}
/>
{isOwnerOrAdmin && <CreateProjectButton />}
</Flex>
@@ -657,7 +797,7 @@ const ProjectList: React.FC = () => {
size="small"
onChange={handleTableChange}
pagination={paginationConfig}
locale={{ emptyText }}
locale={{ emptyText: emptyContent }}
onRow={record => ({
onClick: () => navigateToProject(record.id, record.team_member_default_view),
onMouseEnter: () => handleProjectHover(record.id),

View File

@@ -1,582 +0,0 @@
import { useEffect, useState, useRef, useMemo, useCallback } from 'react';
import { useAppSelector } from '@/hooks/useAppSelector';
import TaskListFilters from '../taskList/task-list-filters/task-list-filters';
import { Flex, Skeleton } from 'antd';
import BoardSectionCardContainer from './board-section/board-section-container';
import {
fetchBoardTaskGroups,
reorderTaskGroups,
moveTaskBetweenGroups,
IGroupBy,
updateTaskProgress,
} from '@features/board/board-slice';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import {
DndContext,
DragEndEvent,
DragOverEvent,
DragStartEvent,
closestCenter,
DragOverlay,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
getFirstCollision,
pointerWithin,
rectIntersection,
UniqueIdentifier,
} from '@dnd-kit/core';
import BoardViewTaskCard from './board-section/board-task-card/board-view-task-card';
import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
import useTabSearchParam from '@/hooks/useTabSearchParam';
import { useSocket } from '@/socket/socketContext';
import { useAuthService } from '@/hooks/useAuth';
import { SocketEvents } from '@/shared/socket-events';
import alertService from '@/services/alerts/alertService';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
import {
evt_project_board_visit,
evt_project_task_list_drag_and_move,
} from '@/shared/worklenz-analytics-events';
import { ITaskStatusCreateRequest } from '@/types/tasks/task-status-create-request';
import { statusApiService } from '@/api/taskAttributes/status/status.api.service';
import logger from '@/utils/errorLogger';
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: {
current?: {
type?: string;
};
};
}
const ProjectViewBoard = () => {
const dispatch = useAppDispatch();
const { projectView } = useTabSearchParam();
const { socket } = useSocket();
const authService = useAuthService();
const currentSession = authService.getCurrentSession();
const { trackMixpanelEvent } = useMixpanelTracking();
const [currentTaskIndex, setCurrentTaskIndex] = useState(-1);
// Add local loading state to immediately show skeleton
const [isLoading, setIsLoading] = useState(true);
const { projectId } = useAppSelector(state => state.projectReducer);
const { taskGroups, groupBy, loadingGroups, search, archived } = useAppSelector(
state => state.boardReducer
);
const { statusCategories, loading: loadingStatusCategories } = useAppSelector(
state => state.taskStatusReducer
);
const [activeItem, setActiveItem] = useState<any>(null);
// Store the original source group ID when drag starts
const originalSourceGroupIdRef = useRef<string | null>(null);
const lastOverId = useRef<UniqueIdentifier | null>(null);
const recentlyMovedToNewContainer = useRef(false);
const [clonedItems, setClonedItems] = useState<any>(null);
const isDraggingRef = useRef(false);
// Update loading state based on all loading conditions
useEffect(() => {
setIsLoading(loadingGroups || loadingStatusCategories);
}, [loadingGroups, loadingStatusCategories]);
// Load data efficiently with async/await and Promise.all
useEffect(() => {
const loadData = async () => {
if (projectId && groupBy && projectView === 'kanban') {
const promises = [];
if (!loadingGroups) {
promises.push(dispatch(fetchBoardTaskGroups(projectId)));
}
if (!statusCategories.length) {
promises.push(dispatch(fetchStatusesCategories()));
}
// Wait for all data to load
await Promise.all(promises);
}
};
loadData();
}, [dispatch, projectId, groupBy, projectView, search, archived]);
// Create sensors with memoization to prevent unnecessary re-renders
const sensors = useSensors(
useSensor(MouseSensor, {
activationConstraint: {
distance: 10,
delay: 100,
tolerance: 5,
},
}),
useSensor(TouchSensor, {
activationConstraint: {
delay: 250,
tolerance: 5,
},
})
);
const collisionDetectionStrategy = useCallback(
(args: {
active: { id: UniqueIdentifier; data: { current?: { type?: string } } };
droppableContainers: DroppableContainer[];
}) => {
if (activeItem?.type === 'section') {
return closestCenter({
...args,
droppableContainers: args.droppableContainers.filter(
(container: DroppableContainer) => container.data.current?.type === 'section'
),
});
}
// Start by finding any intersecting droppable
const pointerIntersections = pointerWithin(args);
const intersections =
pointerIntersections.length > 0 ? pointerIntersections : rectIntersection(args);
let overId = getFirstCollision(intersections, 'id');
if (overId !== null) {
const overContainer = args.droppableContainers.find(
(container: DroppableContainer) => container.id === overId
);
if (overContainer?.data.current?.type === 'section') {
const containerItems = taskGroups.find(group => group.id === overId)?.tasks || [];
if (containerItems.length > 0) {
overId = closestCenter({
...args,
droppableContainers: args.droppableContainers.filter(
(container: DroppableContainer) =>
container.id !== overId && container.data.current?.type === 'task'
),
})[0]?.id;
}
}
lastOverId.current = overId;
return [{ id: overId }];
}
if (recentlyMovedToNewContainer.current) {
lastOverId.current = activeItem?.id;
}
return lastOverId.current ? [{ id: lastOverId.current }] : [];
},
[activeItem, taskGroups]
);
const handleTaskProgress = (data: {
id: string;
status: string;
complete_ratio: number;
completed_count: number;
total_tasks_count: number;
parent_task: string;
}) => {
dispatch(updateTaskProgress(data));
};
// Debounced move task function to prevent rapid updates
const debouncedMoveTask = useCallback(
debounce(
(taskId: string, sourceGroupId: string, targetGroupId: string, targetIndex: number) => {
dispatch(
moveTaskBetweenGroups({
taskId,
sourceGroupId,
targetGroupId,
targetIndex,
})
);
},
100
),
[dispatch]
);
const handleDragStart = (event: DragStartEvent) => {
const { active } = event;
isDraggingRef.current = true;
setActiveItem(active.data.current);
setCurrentTaskIndex(active.data.current?.sortable.index);
if (active.data.current?.type === 'task') {
originalSourceGroupIdRef.current = active.data.current.sectionId;
}
setClonedItems(taskGroups);
};
const findGroupForId = (id: string) => {
// If id is a sectionId
if (taskGroups.some(group => group.id === id)) return id;
// If id is a taskId, find the group containing it
const group = taskGroups.find(g => g.tasks.some(t => t.id === id));
return group?.id;
};
const handleDragOver = (event: DragOverEvent) => {
try {
if (!isDraggingRef.current) return;
const { active, over } = event;
if (!over) return;
// Get the ids
const activeId = active.id;
const overId = over.id;
// Find the group (section) for each
const activeGroupId = findGroupForId(activeId as string);
const overGroupId = findGroupForId(overId as string);
// Only move if both groups exist and are different, and the active is a task
if (activeGroupId && overGroupId && active.data.current?.type === 'task') {
// Find the target index in the over group
const targetGroup = taskGroups.find(g => g.id === overGroupId);
let targetIndex = 0;
if (targetGroup) {
// If over is a task, insert before it; if over is a section, append to end
if (over.data.current?.type === 'task') {
targetIndex = targetGroup.tasks.findIndex(t => t.id === overId);
if (targetIndex === -1) targetIndex = targetGroup.tasks.length;
} else {
targetIndex = targetGroup.tasks.length;
}
}
// Use debounced move task to prevent rapid updates
debouncedMoveTask(activeId as string, activeGroupId, overGroupId, targetIndex);
}
} catch (error) {
console.error('handleDragOver error:', error);
}
};
const handlePriorityChange = (taskId: string, priorityId: string) => {
if (!taskId || !priorityId || !socket) return;
const payload = {
task_id: taskId,
priority_id: priorityId,
team_id: currentSession?.team_id,
};
socket.emit(SocketEvents.TASK_PRIORITY_CHANGE.toString(), JSON.stringify(payload));
socket.once(
SocketEvents.TASK_PRIORITY_CHANGE.toString(),
(data: ITaskListPriorityChangeResponse) => {
dispatch(updateBoardTaskPriority(data));
}
);
};
const handleDragEnd = async (event: DragEndEvent) => {
isDraggingRef.current = false;
const { active, over } = event;
if (!over || !projectId) {
setActiveItem(null);
originalSourceGroupIdRef.current = null;
setClonedItems(null);
return;
}
const isActiveTask = active.data.current?.type === 'task';
const isActiveSection = active.data.current?.type === 'section';
// Handle task dragging between columns
if (isActiveTask) {
const task = active.data.current?.task;
// Use the original source group ID from ref instead of the potentially modified one
const sourceGroupId = originalSourceGroupIdRef.current || active.data.current?.sectionId;
// Fix: Ensure we correctly identify the target group ID
let targetGroupId;
if (over.data.current?.type === 'task') {
// If dropping on a task, get its section ID
targetGroupId = over.data.current?.sectionId;
} else if (over.data.current?.type === 'section') {
// If dropping directly on a section
targetGroupId = over.id;
} else {
// Fallback to the over ID if type is not specified
targetGroupId = over.id;
}
// Find source and target groups
const sourceGroup = taskGroups.find(group => group.id === sourceGroupId);
const targetGroup = taskGroups.find(group => group.id === targetGroupId);
if (!sourceGroup || !targetGroup || !task) {
logger.error('Could not find source or target group, or task is undefined');
setActiveItem(null);
originalSourceGroupIdRef.current = null; // Reset the ref
return;
}
if (targetGroupId !== sourceGroupId) {
const canContinue = await checkTaskDependencyStatus(task.id, targetGroupId);
if (!canContinue) {
alertService.error(
'Task is not completed',
'Please complete the task dependencies before proceeding'
);
dispatch(
moveTaskBetweenGroups({
taskId: task.id,
sourceGroupId: targetGroupId, // Current group (where it was moved optimistically)
targetGroupId: sourceGroupId, // Move it back to the original source group
targetIndex: currentTaskIndex !== -1 ? currentTaskIndex : 0, // Original position or append to end
})
);
setActiveItem(null);
originalSourceGroupIdRef.current = null;
return;
}
}
// Find indices
let fromIndex = sourceGroup.tasks.findIndex(t => t.id === task.id);
// Handle case where task is not found in source group (might have been moved already in UI)
if (fromIndex === -1) {
logger.info('Task not found in source group. Using task sort_order from task object.');
// Use the sort_order from the task object itself
const fromSortOrder = task.sort_order;
// Calculate target index and position
let toIndex = -1;
if (over.data.current?.type === 'task') {
const overTaskId = over.data.current?.task.id;
toIndex = targetGroup.tasks.findIndex(t => t.id === overTaskId);
} else {
// If dropping on a section, append to the end
toIndex = targetGroup.tasks.length;
}
// Calculate toPos similar to Angular implementation
const toPos =
targetGroup.tasks[toIndex]?.sort_order ||
targetGroup.tasks[targetGroup.tasks.length - 1]?.sort_order ||
-1;
// Prepare socket event payload
const body = {
project_id: projectId,
from_index: fromSortOrder,
to_index: toPos,
to_last_index: !toPos,
from_group: sourceGroupId,
to_group: targetGroupId,
group_by: groupBy || 'status',
task,
team_id: currentSession?.team_id,
};
// Emit socket event
if (socket) {
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), body);
// Set up listener for task progress update
socket.once(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), () => {
if (task.is_sub_task) {
socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.parent_task_id);
} else {
socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id);
}
});
// Handle priority change if groupBy is priority
if (groupBy === IGroupBy.PRIORITY) {
handlePriorityChange(task.id, targetGroupId);
}
}
// Track analytics event
trackMixpanelEvent(evt_project_task_list_drag_and_move);
setActiveItem(null);
originalSourceGroupIdRef.current = null; // Reset the ref
return;
}
// Calculate target index and position
let toIndex = -1;
if (over.data.current?.type === 'task') {
const overTaskId = over.data.current?.task.id;
toIndex = targetGroup.tasks.findIndex(t => t.id === overTaskId);
} else {
// If dropping on a section, append to the end
toIndex = targetGroup.tasks.length;
}
// Calculate toPos similar to Angular implementation
const toPos =
targetGroup.tasks[toIndex]?.sort_order ||
targetGroup.tasks[targetGroup.tasks.length - 1]?.sort_order ||
-1;
// Prepare socket event payload
const body = {
project_id: projectId,
from_index: sourceGroup.tasks[fromIndex].sort_order,
to_index: toPos,
to_last_index: !toPos,
from_group: sourceGroupId, // Use the direct IDs instead of group objects
to_group: targetGroupId, // Use the direct IDs instead of group objects
group_by: groupBy || 'status', // Use the current groupBy value
task,
team_id: currentSession?.team_id,
};
// Emit socket event
if (socket) {
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), body);
// Set up listener for task progress update
socket.once(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), () => {
if (task.is_sub_task) {
socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.parent_task_id);
} else {
socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id);
}
});
}
// Track analytics event
trackMixpanelEvent(evt_project_task_list_drag_and_move);
}
// Handle column reordering
else if (isActiveSection) {
// Don't allow reordering if groupBy is phases
if (groupBy === IGroupBy.PHASE) {
setActiveItem(null);
originalSourceGroupIdRef.current = null;
return;
}
const sectionId = active.id;
const fromIndex = taskGroups.findIndex(group => group.id === sectionId);
const toIndex = taskGroups.findIndex(group => group.id === over.id);
if (fromIndex !== -1 && toIndex !== -1) {
// Create a new array with the reordered groups
const reorderedGroups = [...taskGroups];
const [movedGroup] = reorderedGroups.splice(fromIndex, 1);
reorderedGroups.splice(toIndex, 0, movedGroup);
// Dispatch action to reorder columns with the new array
dispatch(reorderTaskGroups(reorderedGroups));
// Prepare column order for API
const columnOrder = reorderedGroups.map(group => group.id);
// Call API to update status order
try {
// Use the correct API endpoint based on the Angular code
const requestBody: ITaskStatusCreateRequest = {
status_order: columnOrder,
};
const response = await statusApiService.updateStatusOrder(requestBody, projectId);
if (!response.done) {
const revertedGroups = [...reorderedGroups];
const [movedBackGroup] = revertedGroups.splice(toIndex, 1);
revertedGroups.splice(fromIndex, 0, movedBackGroup);
dispatch(reorderTaskGroups(revertedGroups));
alertService.error('Failed to update column order', 'Please try again');
}
} catch (error) {
// Revert the change if API call fails
const revertedGroups = [...reorderedGroups];
const [movedBackGroup] = revertedGroups.splice(toIndex, 1);
revertedGroups.splice(fromIndex, 0, movedBackGroup);
dispatch(reorderTaskGroups(revertedGroups));
alertService.error('Failed to update column order', 'Please try again');
}
}
}
setActiveItem(null);
originalSourceGroupIdRef.current = null; // Reset the ref
};
const handleDragCancel = () => {
isDraggingRef.current = false;
if (clonedItems) {
dispatch(reorderTaskGroups(clonedItems));
}
setActiveItem(null);
setClonedItems(null);
originalSourceGroupIdRef.current = null;
};
// Reset the recently moved flag after animation frame
useEffect(() => {
requestAnimationFrame(() => {
recentlyMovedToNewContainer.current = false;
});
}, [taskGroups]);
useEffect(() => {
if (socket) {
socket.on(SocketEvents.GET_TASK_PROGRESS.toString(), handleTaskProgress);
}
return () => {
socket?.off(SocketEvents.GET_TASK_PROGRESS.toString(), handleTaskProgress);
};
}, [socket]);
// Track analytics event on component mount
useEffect(() => {
trackMixpanelEvent(evt_project_board_visit);
}, []);
// Cleanup debounced function on unmount
useEffect(() => {
return () => {
debouncedMoveTask.cancel();
};
}, [debouncedMoveTask]);
return (
<Flex vertical gap={16}>
<TaskListFilters position={'board'} />
<Skeleton active loading={isLoading} className="mt-4 p-4">
<DndContext
sensors={sensors}
collisionDetection={collisionDetectionStrategy}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<BoardSectionCardContainer
datasource={taskGroups}
group={groupBy as 'status' | 'priority' | 'phases'}
/>
<DragOverlay>
{activeItem?.type === 'task' && (
<BoardViewTaskCard task={activeItem.task} sectionId={activeItem.sectionId} />
)}
</DragOverlay>
</DndContext>
</Skeleton>
</Flex>
);
};
export default ProjectViewBoard;

View File

@@ -1,19 +0,0 @@
import React from 'react';
import { useAppSelector } from '@/hooks/useAppSelector';
import TaskListBoard from '@/components/task-management/task-list-board';
const ProjectViewEnhancedTasks: React.FC = () => {
const { project } = useAppSelector(state => state.projectReducer);
if (!project?.id) {
return <div className="p-4 text-center text-gray-500">Project not found</div>;
}
return (
<div className="project-view-enhanced-tasks">
<TaskListBoard projectId={project.id} />
</div>
);
};
export default ProjectViewEnhancedTasks;

View File

@@ -181,9 +181,24 @@ const ProjectViewHeader = memo(() => {
// Memoized settings handler
const handleSettingsClick = useCallback(() => {
if (selectedProject?.id) {
console.log('Opening project drawer from project view for project:', selectedProject.id);
// Set project ID first
dispatch(setProjectId(selectedProject.id));
dispatch(fetchProjectData(selectedProject.id));
// Then fetch project data
dispatch(fetchProjectData(selectedProject.id))
.unwrap()
.then((projectData) => {
console.log('Project data fetched successfully from project view:', projectData);
// Open drawer after data is fetched
dispatch(toggleProjectDrawer());
})
.catch((error) => {
console.error('Failed to fetch project data from project view:', error);
// Still open drawer even if fetch fails, so user can see error state
dispatch(toggleProjectDrawer());
});
}
}, [dispatch, selectedProject?.id]);

View File

@@ -1,128 +0,0 @@
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';
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';
import { fetchTaskGroups, fetchTaskListColumns } from '@/features/tasks/tasks.slice';
import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/phases.slice';
import useTabSearchParam from '@/hooks/useTabSearchParam';
const ProjectViewTaskList = () => {
const dispatch = useAppDispatch();
const { projectView } = useTabSearchParam();
const [searchParams, setSearchParams] = useSearchParams();
const [coreDataLoaded, setCoreDataLoaded] = useState(false);
// Split selectors to prevent unnecessary rerenders
const projectId = useAppSelector(state => state.projectReducer.projectId);
const taskGroups = useAppSelector(state => state.taskReducer.taskGroups);
const loadingGroups = useAppSelector(state => state.taskReducer.loadingGroups);
const groupBy = useAppSelector(state => state.taskReducer.groupBy);
const archived = useAppSelector(state => state.taskReducer.archived);
const fields = useAppSelector(state => state.taskReducer.fields);
const search = useAppSelector(state => state.taskReducer.search);
const statusCategories = useAppSelector(state => state.taskStatusReducer.statusCategories);
const loadingStatusCategories = useAppSelector(state => state.taskStatusReducer.loading);
const loadingPhases = useAppSelector(state => state.phaseReducer.loadingPhases);
// Simplified loading state - only wait for essential data
// Remove dependency on phases and status categories for initial render
const isLoading = useMemo(
() => loadingGroups || !coreDataLoaded,
[loadingGroups, coreDataLoaded]
);
// Memoize the empty state check
const isEmptyState = useMemo(
() => taskGroups && taskGroups.length === 0 && !isLoading,
[taskGroups, isLoading]
);
// Handle view type changes
useEffect(() => {
if (projectView !== 'list' && projectView !== 'board') {
const newParams = new URLSearchParams(searchParams);
newParams.set('tab', 'tasks-list');
newParams.set('pinned_tab', 'tasks-list');
setSearchParams(newParams);
}
}, [projectView, setSearchParams, searchParams]);
// Optimized parallel data fetching - don't wait for everything
useEffect(() => {
const fetchCoreData = async () => {
if (!projectId || !groupBy || coreDataLoaded) return;
try {
// 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(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 core data:', error);
setCoreDataLoaded(true); // Still mark as complete to prevent infinite loading
}
};
fetchCoreData();
}, [projectId, groupBy, dispatch, coreDataLoaded]);
// Optimized task groups fetching - remove initialLoadComplete dependency
useEffect(() => {
const fetchTasks = async () => {
if (!projectId || !groupBy || projectView !== 'list') return;
try {
// Only refetch if filters change, not on initial load
if (coreDataLoaded) {
await dispatch(fetchTaskGroups(projectId));
}
} catch (error) {
console.error('Error fetching task groups:', error);
}
};
// 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 synchronously - no suspense boundary */}
<TaskListFilters position="list" />
{isEmptyState ? (
<Empty description="No tasks group found" />
) : (
<Skeleton active loading={isLoading} className="mt-4 p-4">
<TaskGroupWrapperOptimized taskGroups={memoizedTaskGroups} groupBy={groupBy} />
</Skeleton>
)}
</Flex>
);
};
export default ProjectViewTaskList;

View File

@@ -1,82 +0,0 @@
import React, { useEffect, useMemo } from 'react';
import { createPortal } from 'react-dom';
import Flex from 'antd/es/flex';
import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect';
import { ITaskListGroup } from '@/types/tasks/taskList.types';
import { useAppSelector } from '@/hooks/useAppSelector';
import TaskListTableWrapper from './task-list-table/task-list-table-wrapper/task-list-table-wrapper';
import TaskTemplateDrawer from '@/components/task-templates/task-template-drawer';
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
interface TaskGroupWrapperOptimizedProps {
taskGroups: ITaskListGroup[];
groupBy: string;
}
const TaskGroupWrapperOptimized = ({ taskGroups, groupBy }: TaskGroupWrapperOptimizedProps) => {
const themeMode = useAppSelector((state: any) => state.themeReducer.mode);
// Use extracted hooks
useTaskSocketHandlers();
// Memoize task groups with colors
const taskGroupsWithColors = useMemo(
() =>
taskGroups?.map(taskGroup => ({
...taskGroup,
displayColor: themeMode === 'dark' ? taskGroup.color_code_dark : taskGroup.color_code,
})) || [],
[taskGroups, themeMode]
);
// Add drag styles without animations
useEffect(() => {
const style = document.createElement('style');
style.textContent = `
.task-row[data-is-dragging="true"] {
opacity: 0.5 !important;
z-index: 1000 !important;
position: relative !important;
}
.task-row {
/* Remove transitions during drag operations */
}
`;
document.head.appendChild(style);
return () => {
document.head.removeChild(style);
};
}, []);
// Remove the animation cleanup since we're simplifying the approach
return (
<Flex gap={24} vertical>
{taskGroupsWithColors.map(taskGroup => (
<TaskListTableWrapper
key={taskGroup.id}
taskList={taskGroup.tasks}
tableId={taskGroup.id}
name={taskGroup.name}
groupBy={groupBy}
statusCategory={taskGroup.category_id}
color={taskGroup.displayColor}
activeId={null}
/>
))}
{createPortal(
<TaskTemplateDrawer showDrawer={false} selectedTemplateId="" onClose={() => {}} />,
document.body,
'task-template-drawer'
)}
</Flex>
);
};
export default React.memo(TaskGroupWrapperOptimized);