import { Button, Dropdown, Flex, Tag, Tooltip, Typography, ArrowLeftOutlined, BellFilled, BellOutlined, CalendarOutlined, DownOutlined, EditOutlined, ImportOutlined, SaveOutlined, SettingOutlined, SyncOutlined, UsergroupAddOutlined, } from '@/shared/antd-imports'; import { PageHeader } from '@ant-design/pro-components'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { useState, useCallback, useMemo, memo, useRef, useEffect } from 'react'; import { createPortal } from 'react-dom'; import { colors } from '@/styles/colors'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppSelector } from '@/hooks/useAppSelector'; import { SocketEvents } from '@/shared/socket-events'; import { useAuthService } from '@/hooks/useAuth'; import { useSocket } from '@/socket/socketContext'; import { setProject, setImportTaskTemplateDrawerOpen, setRefreshTimestamp, getProject, } from '@features/project/project.slice'; import { addTask, fetchTaskGroups, fetchTaskListColumns, IGroupBy, } from '@features/tasks/tasks.slice'; import ProjectStatusIcon from '@/components/common/project-status-icon/project-status-icon'; import { formatDate } from '@/utils/timeUtils'; import { toggleSaveAsTemplateDrawer } from '@/features/projects/projectsSlice'; import SaveProjectAsTemplate from '@/components/save-project-as-template/save-project-as-template'; import { fetchProjectData, toggleProjectDrawer, setProjectId, } from '@/features/project/project-drawer.slice'; import { setSelectedTaskId, setShowTaskDrawer } from '@/features/task-drawer/task-drawer.slice'; import { ITaskCreateRequest } from '@/types/tasks/task-create-request.types'; import { DEFAULT_TASK_NAME, UNMAPPED } from '@/shared/constants'; import { IProjectTask } from '@/types/project/projectTasksViewModel.types'; import { getGroupIdByGroupedColumn } from '@/services/task-list/taskList.service'; import logger from '@/utils/errorLogger'; import ImportTaskTemplate from '@/components/task-templates/import-task-template'; import ProjectDrawer from '@/components/projects/project-drawer/project-drawer'; import { toggleProjectMemberDrawer } from '@/features/projects/singleProject/members/projectMembersSlice'; import useIsProjectManager from '@/hooks/useIsProjectManager'; import useTabSearchParam from '@/hooks/useTabSearchParam'; import { addTaskCardToTheTop, fetchBoardTaskGroups } from '@/features/board/board-slice'; import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/phases.slice'; import { fetchEnhancedKanbanGroups } from '@/features/enhanced-kanban/enhanced-kanban.slice'; import { fetchTasksV3 } from '@/features/task-management/task-management.slice'; const ProjectViewHeader = memo(() => { const navigate = useNavigate(); const { t } = useTranslation('project-view/project-view-header'); const dispatch = useAppDispatch(); const { tab } = useTabSearchParam(); // Memoize auth service calls to prevent unnecessary re-evaluations const authService = useAuthService(); const currentSession = useMemo(() => authService.getCurrentSession(), [authService]); const isOwnerOrAdmin = useMemo(() => authService.isOwnerOrAdmin(), [authService]); const isProjectManager = useIsProjectManager(); const { socket } = useSocket(); // Optimized selectors with shallow equality checks const selectedProject = useAppSelector(state => state.projectReducer.project); const projectId = useAppSelector(state => state.projectReducer.projectId); const loadingGroups = useAppSelector(state => state.taskReducer.loadingGroups); const groupBy = useAppSelector(state => state.taskReducer.groupBy); const [creatingTask, setCreatingTask] = useState(false); const [subscriptionLoading, setSubscriptionLoading] = useState(false); // Use ref to track subscription timeout const subscriptionTimeoutRef = useRef(null); // Memoized refresh handler with optimized dependencies const handleRefresh = useCallback(() => { if (!projectId) return; dispatch(getProject(projectId)); switch (tab) { case 'tasks-list': dispatch(fetchTaskListColumns(projectId)); dispatch(fetchPhasesByProjectId(projectId)); dispatch(fetchTaskGroups(projectId)); dispatch(fetchTasksV3(projectId)); break; case 'board': dispatch(fetchEnhancedKanbanGroups(projectId)); break; case 'project-insights-member-overview': case 'all-attachments': case 'members': case 'updates': dispatch(setRefreshTimestamp()); break; } }, [dispatch, projectId, tab]); // Optimized subscription handler with proper cleanup const handleSubscribe = useCallback(() => { if (!selectedProject?.id || !socket || subscriptionLoading) return; try { setSubscriptionLoading(true); const newSubscriptionState = !selectedProject.subscribed; // Clear any existing timeout if (subscriptionTimeoutRef.current) { clearTimeout(subscriptionTimeoutRef.current); } // Emit socket event socket.emit(SocketEvents.PROJECT_SUBSCRIBERS_CHANGE.toString(), { project_id: selectedProject.id, user_id: currentSession?.id, team_member_id: currentSession?.team_member_id, mode: newSubscriptionState ? 0 : 1, }); // Listen for response with cleanup const handleResponse = (response: any) => { try { dispatch( setProject({ ...selectedProject, subscribed: newSubscriptionState, }) ); } catch (error) { logger.error('Error handling project subscription response:', error); dispatch( setProject({ ...selectedProject, subscribed: selectedProject.subscribed, }) ); } finally { setSubscriptionLoading(false); if (subscriptionTimeoutRef.current) { clearTimeout(subscriptionTimeoutRef.current); subscriptionTimeoutRef.current = null; } } }; socket.once(SocketEvents.PROJECT_SUBSCRIBERS_CHANGE.toString(), handleResponse); // Set timeout with ref tracking subscriptionTimeoutRef.current = setTimeout(() => { setSubscriptionLoading(false); logger.error('Project subscription timeout - no response from server'); subscriptionTimeoutRef.current = null; }, 5000); } catch (error) { logger.error('Error updating project subscription:', error); setSubscriptionLoading(false); } }, [selectedProject, socket, subscriptionLoading, currentSession, dispatch]); // Memoized settings handler const handleSettingsClick = useCallback(() => { if (selectedProject?.id) { dispatch(setProjectId(selectedProject.id)); dispatch(fetchProjectData(selectedProject.id)); dispatch(toggleProjectDrawer()); } }, [dispatch, selectedProject?.id]); // Optimized task creation handler const handleCreateTask = useCallback(() => { if (!selectedProject?.id || !currentSession?.id || !socket) return; try { setCreatingTask(true); const body: Partial = { name: DEFAULT_TASK_NAME, project_id: selectedProject.id, reporter_id: currentSession.id, team_id: currentSession.team_id, }; const handleTaskCreated = (task: IProjectTask) => { if (task.id) { dispatch(setSelectedTaskId(task.id)); dispatch(setShowTaskDrawer(true)); const groupId = groupBy === IGroupBy.PHASE ? UNMAPPED : getGroupIdByGroupedColumn(task); if (groupId) { if (tab === 'board') { dispatch(addTaskCardToTheTop({ sectionId: groupId, task })); } else { dispatch(addTask({ task, groupId })); } socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id); } } setCreatingTask(false); }; socket.once(SocketEvents.QUICK_TASK.toString(), handleTaskCreated); socket.emit(SocketEvents.QUICK_TASK.toString(), JSON.stringify(body)); } catch (error) { logger.error('Error creating task', error); setCreatingTask(false); } }, [selectedProject?.id, currentSession, socket, dispatch, groupBy, tab]); // Memoized import task template handler const handleImportTaskTemplate = useCallback(() => { dispatch(setImportTaskTemplateDrawerOpen(true)); }, [dispatch]); // Memoized navigation handler const handleNavigateToProjects = useCallback(() => { navigate('/worklenz/projects'); }, [navigate]); // Memoized save as template handler const handleSaveAsTemplate = useCallback(() => { dispatch(toggleSaveAsTemplateDrawer()); }, [dispatch]); // Memoized invite handler const handleInvite = useCallback(() => { dispatch(toggleProjectMemberDrawer()); }, [dispatch]); // Memoized dropdown items const dropdownItems = useMemo( () => [ { key: 'import', label: (
{t('importTask')}
), }, ], [handleImportTaskTemplate, t] ); // Memoized project attributes with optimized date formatting const projectAttributes = useMemo(() => { if (!selectedProject) return null; const elements = []; if (selectedProject.category_id) { elements.push( {selectedProject.category_name} ); } if (selectedProject.status) { elements.push( ); } if (selectedProject.start_date || selectedProject.end_date) { const tooltipContent = ( {selectedProject.start_date && `${t('startDate')}: ${formatDate(new Date(selectedProject.start_date))}`} {selectedProject.end_date && ( <>
{`${t('endDate')}: ${formatDate(new Date(selectedProject.end_date))}`} )}
); elements.push( ); } if (selectedProject.notes) { elements.push( {selectedProject.notes} ); } return ( {elements} ); }, [selectedProject, t]); // Memoized header actions with conditional rendering optimization const headerActions = useMemo(() => { const actions = []; // Refresh button actions.push( ); // Invite button (owner/admin/project manager only) if (isOwnerOrAdmin || isProjectManager) { actions.push( ); } // Create task button if (isOwnerOrAdmin) { actions.push( } menu={{ items: dropdownItems }} trigger={['click']} onClick={handleCreateTask} > {t('createTask')} ); } else { actions.push( ); } return ( {actions} ); }, [ loadingGroups, handleRefresh, isOwnerOrAdmin, handleSaveAsTemplate, handleSettingsClick, t, subscriptionLoading, selectedProject?.subscribed, handleSubscribe, isProjectManager, handleInvite, creatingTask, dropdownItems, handleCreateTask, ]); // Memoized page header title const pageHeaderTitle = useMemo( () => ( {selectedProject?.name} {projectAttributes} ), [handleNavigateToProjects, selectedProject?.name, projectAttributes] ); // Memoized page header styles const pageHeaderStyle = useMemo( () => ({ paddingInline: 0, marginBlockEnd: 12, }), [] ); // Cleanup timeout on unmount useEffect(() => { return () => { if (subscriptionTimeoutRef.current) { clearTimeout(subscriptionTimeoutRef.current); } }; }, []); return ( <> {createPortal( {}} />, document.body, 'project-drawer')} {createPortal(, document.body, 'import-task-template')} {createPortal(, document.body, 'save-project-as-template')} ); }); ProjectViewHeader.displayName = 'ProjectViewHeader'; export default ProjectViewHeader;