From b0253135e5c7b87fa4db8aad8af71022aacf3842 Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Mon, 7 Jul 2025 17:07:45 +0530 Subject: [PATCH] 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. --- worklenz-frontend/package-lock.json | 26 + .../project-group/project-group-list.tsx | 19 +- .../project-list-actions.tsx | 19 +- .../project-drawer/project-drawer.tsx | 103 +++- .../features/project/project-drawer.slice.ts | 32 +- .../src/lib/project/project-view-constants.ts | 1 - .../src/pages/projects/project-list.tsx | 404 ++++++++---- .../projectView/board/project-view-board.tsx | 582 ------------------ .../project-view-enhanced-tasks.tsx | 19 - .../projectView/project-view-header.tsx | 19 +- .../taskList/project-view-task-list.tsx | 128 ---- .../taskList/task-group-wrapper-optimized.tsx | 82 --- 12 files changed, 450 insertions(+), 984 deletions(-) delete mode 100644 worklenz-frontend/src/pages/projects/projectView/board/project-view-board.tsx delete mode 100644 worklenz-frontend/src/pages/projects/projectView/enhancedTasks/project-view-enhanced-tasks.tsx delete mode 100644 worklenz-frontend/src/pages/projects/projectView/taskList/project-view-task-list.tsx delete mode 100644 worklenz-frontend/src/pages/projects/projectView/taskList/task-group-wrapper-optimized.tsx diff --git a/worklenz-frontend/package-lock.json b/worklenz-frontend/package-lock.json index 5e5154f3..0aba31ba 100644 --- a/worklenz-frontend/package-lock.json +++ b/worklenz-frontend/package-lock.json @@ -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", diff --git a/worklenz-frontend/src/components/project-list/project-group/project-group-list.tsx b/worklenz-frontend/src/components/project-list/project-group/project-group-list.tsx index 1e919118..bb07669d 100644 --- a/worklenz-frontend/src/components/project-list/project-group/project-group-list.tsx +++ b/worklenz-frontend/src/components/project-list/project-group/project-group-list.tsx @@ -124,10 +124,25 @@ const ProjectGroupList: React.FC = ({ // 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)); - dispatch(toggleProjectDrawer()); + + // 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 ( diff --git a/worklenz-frontend/src/components/project-list/project-list-table/project-list-actions/project-list-actions.tsx b/worklenz-frontend/src/components/project-list/project-list-table/project-list-actions/project-list-actions.tsx index 57c14e36..c447ddeb 100644 --- a/worklenz-frontend/src/components/project-list/project-list-table/project-list-actions/project-list-actions.tsx +++ b/worklenz-frontend/src/components/project-list/project-list-table/project-list-actions/project-list-actions.tsx @@ -46,10 +46,25 @@ export const ActionButtons: React.FC = ({ 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)); - dispatch(toggleProjectDrawer()); + + // 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()); + }); } }; diff --git a/worklenz-frontend/src/components/projects/project-drawer/project-drawer.tsx b/worklenz-frontend/src/components/projects/project-drawer/project-drawer.tsx index 7bae3717..f6519cb9 100644 --- a/worklenz-frontend/src/components/projects/project-drawer/project-drawer.tsx +++ b/worklenz-frontend/src/components/projects/project-drawer/project-drawer.tsx @@ -72,6 +72,7 @@ const ProjectDrawer = ({ onClose }: { onClose: () => void }) => { null ); const [isFormValid, setIsFormValid] = useState(true); + const [drawerVisible, setDrawerVisible] = useState(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 && ( )} - +
{ 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 }); }, }); diff --git a/worklenz-frontend/src/lib/project/project-view-constants.ts b/worklenz-frontend/src/lib/project/project-view-constants.ts index bf6348a3..5edf1814 100644 --- a/worklenz-frontend/src/lib/project/project-view-constants.ts +++ b/worklenz-frontend/src/lib/project/project-view-constants.ts @@ -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'; diff --git a/worklenz-frontend/src/pages/projects/project-list.tsx b/worklenz-frontend/src/pages/projects/project-list.tsx index 7ea79ba7..a4477493 100644 --- a/worklenz-frontend/src/pages/projects/project-list.tsx +++ b/worklenz-frontend/src/pages/projects/project-list.tsx @@ -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>({}); const [isLoading, setIsLoading] = useState(false); + const [searchValue, setSearchValue] = useState(''); + const searchTimeoutRef = useRef(null); + const lastQueryParamsRef = useRef(''); + const [errorMessage, setErrorMessage] = useState(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) => { + 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(() => , [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, sorter: SorterResult | SorterResult[] ) => { - const newParams: Partial = {}; - if (!filters?.status_id) { - newParams.statuses = null; - dispatch(setFilteredStatuses([])); - } else { - newParams.statuses = filters.status_id.join(' '); + // Batch all parameter updates to reduce re-renders + const updates: Partial = {}; + let hasChanges = false; + + // Handle status filters + if (filters?.status_id !== filteredInfo.status_id) { + if (!filters?.status_id) { + updates.statuses = null; + dispatch(setFilteredStatuses([])); + } else { + updates.statuses = filters.status_id.join(' '); + } + hasChanges = true; } - if (!filters?.category_id) { - newParams.categories = null; - dispatch(setFilteredCategories([])); - } else { - newParams.categories = filters.category_id.join(' '); + // Handle category filters + if (filters?.category_id !== filteredInfo.category_id) { + if (!filters?.category_id) { + updates.categories = null; + dispatch(setFilteredCategories([])); + } else { + 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, - }) - ); + // Also update grouped request params to keep them in sync + dispatch( + setGroupedRequestParams({ + ...groupedRequestParams, + ...updates, + }) + ); + } setFilteredInfo(filters); }, - [dispatch, setSortingValues, groupedRequestParams] + [dispatch, setSortingValues, groupedRequestParams, filteredInfo, requestParams] ); + // Optimized grouped table change handler const handleGroupedTableChange = useCallback( (newPagination: TablePaginationConfig) => { const newParams: Partial = { index: newPagination.current || 1, size: newPagination.pageSize || DEFAULT_PAGE_SIZE, }; - dispatch(setGroupedRequestParams(newParams)); + + // 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 }; + + dispatch(setRequestParams(baseUpdates)); + dispatch(setGroupedRequestParams({ + ...groupedRequestParams, + ...baseUpdates, + })); - // Refresh data based on current view mode - if (viewMode === ProjectViewType.LIST) { - refetchProjects(); - } else if (viewMode === ProjectViewType.GROUP && groupBy) { - dispatch( - fetchGroupedProjects({ - ...groupedRequestParams, - filter: newFilterIndex, - index: 1, - }) - ); + // 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) => { - 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 (
@@ -638,9 +773,14 @@ const ProjectList: React.FC = () => { placeholder={t('placeholder')} suffix={} type="text" - value={requestParams.search} + value={searchValue} onChange={handleSearchChange} aria-label="Search projects" + allowClear + onClear={() => { + setSearchValue(''); + debouncedSearch(''); + }} /> {isOwnerOrAdmin && } @@ -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), diff --git a/worklenz-frontend/src/pages/projects/projectView/board/project-view-board.tsx b/worklenz-frontend/src/pages/projects/projectView/board/project-view-board.tsx deleted file mode 100644 index 63631612..00000000 --- a/worklenz-frontend/src/pages/projects/projectView/board/project-view-board.tsx +++ /dev/null @@ -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(null); - - // Store the original source group ID when drag starts - const originalSourceGroupIdRef = useRef(null); - const lastOverId = useRef(null); - const recentlyMovedToNewContainer = useRef(false); - const [clonedItems, setClonedItems] = useState(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 ( - - - - - - - {activeItem?.type === 'task' && ( - - )} - - - - - ); -}; - -export default ProjectViewBoard; diff --git a/worklenz-frontend/src/pages/projects/projectView/enhancedTasks/project-view-enhanced-tasks.tsx b/worklenz-frontend/src/pages/projects/projectView/enhancedTasks/project-view-enhanced-tasks.tsx deleted file mode 100644 index 9091f19e..00000000 --- a/worklenz-frontend/src/pages/projects/projectView/enhancedTasks/project-view-enhanced-tasks.tsx +++ /dev/null @@ -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
Project not found
; - } - - return ( -
- -
- ); -}; - -export default ProjectViewEnhancedTasks; diff --git a/worklenz-frontend/src/pages/projects/projectView/project-view-header.tsx b/worklenz-frontend/src/pages/projects/projectView/project-view-header.tsx index 5c1a40e1..62db71fe 100644 --- a/worklenz-frontend/src/pages/projects/projectView/project-view-header.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/project-view-header.tsx @@ -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)); - dispatch(toggleProjectDrawer()); + + // 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]); diff --git a/worklenz-frontend/src/pages/projects/projectView/taskList/project-view-task-list.tsx b/worklenz-frontend/src/pages/projects/projectView/taskList/project-view-task-list.tsx deleted file mode 100644 index bff0a697..00000000 --- a/worklenz-frontend/src/pages/projects/projectView/taskList/project-view-task-list.tsx +++ /dev/null @@ -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 ( - - {/* Filters load synchronously - no suspense boundary */} - - - {isEmptyState ? ( - - ) : ( - - - - )} - - ); -}; - -export default ProjectViewTaskList; diff --git a/worklenz-frontend/src/pages/projects/projectView/taskList/task-group-wrapper-optimized.tsx b/worklenz-frontend/src/pages/projects/projectView/taskList/task-group-wrapper-optimized.tsx deleted file mode 100644 index 83c59535..00000000 --- a/worklenz-frontend/src/pages/projects/projectView/taskList/task-group-wrapper-optimized.tsx +++ /dev/null @@ -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 ( - - {taskGroupsWithColors.map(taskGroup => ( - - ))} - - {createPortal( - {}} />, - document.body, - 'task-template-drawer' - )} - - ); -}; - -export default React.memo(TaskGroupWrapperOptimized);