Merge pull request #196 from Worklenz/fix/task-list-realtime-update
feat(localization): update project view header translations and enhan…
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"importTasks": "Importo detyra",
|
"importTasks": "Importo detyra",
|
||||||
|
"importTask": "Importo detyrë",
|
||||||
"createTask": "Krijo detyrë",
|
"createTask": "Krijo detyrë",
|
||||||
"settings": "Cilësimet",
|
"settings": "Cilësimet",
|
||||||
"subscribe": "Abonohu",
|
"subscribe": "Abonohu",
|
||||||
@@ -9,5 +10,8 @@
|
|||||||
"endDate": "Data e përfundimit",
|
"endDate": "Data e përfundimit",
|
||||||
"projectSettings": "Cilësimet e projektit",
|
"projectSettings": "Cilësimet e projektit",
|
||||||
"projectSummary": "Përmbledhja e projektit",
|
"projectSummary": "Përmbledhja e projektit",
|
||||||
"receiveProjectSummary": "Merrni një përmbledhje të projektit çdo mbrëmje."
|
"receiveProjectSummary": "Merrni një përmbledhje të projektit çdo mbrëmje.",
|
||||||
|
"refreshProject": "Rifresko projektin",
|
||||||
|
"saveAsTemplate": "Ruaje si shabllon",
|
||||||
|
"invite": "Fto"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"importTasks": "Aufgaben importieren",
|
"importTasks": "Aufgaben importieren",
|
||||||
|
"importTask": "Aufgabe importieren",
|
||||||
"createTask": "Aufgabe erstellen",
|
"createTask": "Aufgabe erstellen",
|
||||||
"settings": "Einstellungen",
|
"settings": "Einstellungen",
|
||||||
"subscribe": "Abonnieren",
|
"subscribe": "Abonnieren",
|
||||||
@@ -9,5 +10,8 @@
|
|||||||
"endDate": "Enddatum",
|
"endDate": "Enddatum",
|
||||||
"projectSettings": "Projekteinstellungen",
|
"projectSettings": "Projekteinstellungen",
|
||||||
"projectSummary": "Projektzusammenfassung",
|
"projectSummary": "Projektzusammenfassung",
|
||||||
"receiveProjectSummary": "Erhalten Sie jeden Abend eine Projektzusammenfassung."
|
"receiveProjectSummary": "Erhalten Sie jeden Abend eine Projektzusammenfassung.",
|
||||||
|
"refreshProject": "Projekt aktualisieren",
|
||||||
|
"saveAsTemplate": "Als Vorlage speichern",
|
||||||
|
"invite": "Einladen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"importTasks": "Import tasks",
|
"importTasks": "Import tasks",
|
||||||
|
"importTask": "Import task",
|
||||||
"createTask": "Create task",
|
"createTask": "Create task",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"subscribe": "Subscribe",
|
"subscribe": "Subscribe",
|
||||||
@@ -9,5 +10,8 @@
|
|||||||
"endDate": "End date",
|
"endDate": "End date",
|
||||||
"projectSettings": "Project settings",
|
"projectSettings": "Project settings",
|
||||||
"projectSummary": "Project summary",
|
"projectSummary": "Project summary",
|
||||||
"receiveProjectSummary": "Receive a project summary every evening."
|
"receiveProjectSummary": "Receive a project summary every evening.",
|
||||||
|
"refreshProject": "Refresh project",
|
||||||
|
"saveAsTemplate": "Save as template",
|
||||||
|
"invite": "Invite"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"importTasks": "Importar tareas",
|
"importTasks": "Importar tareas",
|
||||||
|
"importTask": "Importar tarea",
|
||||||
"createTask": "Crear tarea",
|
"createTask": "Crear tarea",
|
||||||
"settings": "Ajustes",
|
"settings": "Ajustes",
|
||||||
"subscribe": "Suscribirse",
|
"subscribe": "Suscribirse",
|
||||||
@@ -9,5 +10,8 @@
|
|||||||
"endDate": "Fecha de finalización",
|
"endDate": "Fecha de finalización",
|
||||||
"projectSettings": "Ajustes del proyecto",
|
"projectSettings": "Ajustes del proyecto",
|
||||||
"projectSummary": "Resumen del proyecto",
|
"projectSummary": "Resumen del proyecto",
|
||||||
"receiveProjectSummary": "Recibir un resumen del proyecto todas las noches."
|
"receiveProjectSummary": "Recibir un resumen del proyecto todas las noches.",
|
||||||
|
"refreshProject": "Actualizar proyecto",
|
||||||
|
"saveAsTemplate": "Guardar como plantilla",
|
||||||
|
"invite": "Invitar"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"importTasks": "Importar tarefas",
|
"importTasks": "Importar tarefas",
|
||||||
|
"importTask": "Importar tarefa",
|
||||||
"createTask": "Criar tarefa",
|
"createTask": "Criar tarefa",
|
||||||
"settings": "Configurações",
|
"settings": "Configurações",
|
||||||
"subscribe": "Inscrever-se",
|
"subscribe": "Inscrever-se",
|
||||||
@@ -9,5 +10,8 @@
|
|||||||
"endDate": "Data de fim",
|
"endDate": "Data de fim",
|
||||||
"projectSettings": "Configurações do projeto",
|
"projectSettings": "Configurações do projeto",
|
||||||
"projectSummary": "Resumo do projeto",
|
"projectSummary": "Resumo do projeto",
|
||||||
"receiveProjectSummary": "Receber um resumo do projeto todas as noites."
|
"receiveProjectSummary": "Receber um resumo do projeto todas as noites.",
|
||||||
|
"refreshProject": "Atualizar projeto",
|
||||||
|
"saveAsTemplate": "Salvar como modelo",
|
||||||
|
"invite": "Convidar"
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,8 @@ import {
|
|||||||
import { PageHeader } from '@ant-design/pro-components';
|
import { PageHeader } from '@ant-design/pro-components';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
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 { colors } from '@/styles/colors';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
@@ -39,13 +41,11 @@ import {
|
|||||||
setProjectId,
|
setProjectId,
|
||||||
} from '@/features/project/project-drawer.slice';
|
} from '@/features/project/project-drawer.slice';
|
||||||
import { setSelectedTaskId, setShowTaskDrawer } from '@/features/task-drawer/task-drawer.slice';
|
import { setSelectedTaskId, setShowTaskDrawer } from '@/features/task-drawer/task-drawer.slice';
|
||||||
import { useState } from 'react';
|
|
||||||
import { ITaskCreateRequest } from '@/types/tasks/task-create-request.types';
|
import { ITaskCreateRequest } from '@/types/tasks/task-create-request.types';
|
||||||
import { DEFAULT_TASK_NAME, UNMAPPED } from '@/shared/constants';
|
import { DEFAULT_TASK_NAME, UNMAPPED } from '@/shared/constants';
|
||||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||||
import { getGroupIdByGroupedColumn } from '@/services/task-list/taskList.service';
|
import { getGroupIdByGroupedColumn } from '@/services/task-list/taskList.service';
|
||||||
import logger from '@/utils/errorLogger';
|
import logger from '@/utils/errorLogger';
|
||||||
import { createPortal } from 'react-dom';
|
|
||||||
import ImportTaskTemplate from '@/components/task-templates/import-task-template';
|
import ImportTaskTemplate from '@/components/task-templates/import-task-template';
|
||||||
import ProjectDrawer from '@/components/projects/project-drawer/project-drawer';
|
import ProjectDrawer from '@/components/projects/project-drawer/project-drawer';
|
||||||
import { toggleProjectMemberDrawer } from '@/features/projects/singleProject/members/projectMembersSlice';
|
import { toggleProjectMemberDrawer } from '@/features/projects/singleProject/members/projectMembersSlice';
|
||||||
@@ -56,127 +56,139 @@ import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/
|
|||||||
import { fetchEnhancedKanbanGroups } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
import { fetchEnhancedKanbanGroups } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
import { fetchTasksV3 } from '@/features/task-management/task-management.slice';
|
import { fetchTasksV3 } from '@/features/task-management/task-management.slice';
|
||||||
|
|
||||||
const ProjectViewHeader = () => {
|
const ProjectViewHeader = memo(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation('project-view/project-view-header');
|
const { t } = useTranslation('project-view/project-view-header');
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const currentSession = useAuthService().getCurrentSession();
|
|
||||||
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
|
|
||||||
const isProjectManager = useIsProjectManager();
|
|
||||||
const { tab } = useTabSearchParam();
|
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();
|
const { socket } = useSocket();
|
||||||
|
|
||||||
const {
|
// Optimized selectors with shallow equality checks
|
||||||
project: selectedProject,
|
const selectedProject = useAppSelector(state => state.projectReducer.project);
|
||||||
projectId,
|
const projectId = useAppSelector(state => state.projectReducer.projectId);
|
||||||
} = useAppSelector(state => state.projectReducer);
|
const loadingGroups = useAppSelector(state => state.taskReducer.loadingGroups);
|
||||||
const { loadingGroups, groupBy } = useAppSelector(state => state.taskReducer);
|
const groupBy = useAppSelector(state => state.taskReducer.groupBy);
|
||||||
|
|
||||||
const [creatingTask, setCreatingTask] = useState(false);
|
const [creatingTask, setCreatingTask] = useState(false);
|
||||||
const [subscriptionLoading, setSubscriptionLoading] = useState(false);
|
const [subscriptionLoading, setSubscriptionLoading] = useState(false);
|
||||||
|
|
||||||
const handleRefresh = () => {
|
// Use ref to track subscription timeout
|
||||||
|
const subscriptionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
// Memoized refresh handler with optimized dependencies
|
||||||
|
const handleRefresh = useCallback(() => {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
|
|
||||||
dispatch(getProject(projectId));
|
dispatch(getProject(projectId));
|
||||||
|
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case 'tasks-list':
|
case 'tasks-list':
|
||||||
dispatch(fetchTaskListColumns(projectId));
|
dispatch(fetchTaskListColumns(projectId));
|
||||||
dispatch(fetchPhasesByProjectId(projectId))
|
dispatch(fetchPhasesByProjectId(projectId));
|
||||||
dispatch(fetchTaskGroups(projectId));
|
dispatch(fetchTaskGroups(projectId));
|
||||||
// Also refresh the enhanced tasks data
|
|
||||||
dispatch(fetchTasksV3(projectId));
|
dispatch(fetchTasksV3(projectId));
|
||||||
break;
|
break;
|
||||||
case 'board':
|
case 'board':
|
||||||
// dispatch(fetchBoardTaskGroups(projectId));
|
|
||||||
dispatch(fetchEnhancedKanbanGroups(projectId));
|
dispatch(fetchEnhancedKanbanGroups(projectId));
|
||||||
break;
|
break;
|
||||||
case 'project-insights-member-overview':
|
case 'project-insights-member-overview':
|
||||||
dispatch(setRefreshTimestamp());
|
|
||||||
break;
|
|
||||||
case 'all-attachments':
|
case 'all-attachments':
|
||||||
dispatch(setRefreshTimestamp());
|
|
||||||
break;
|
|
||||||
case 'members':
|
case 'members':
|
||||||
dispatch(setRefreshTimestamp());
|
|
||||||
break;
|
|
||||||
case 'updates':
|
case 'updates':
|
||||||
dispatch(setRefreshTimestamp());
|
dispatch(setRefreshTimestamp());
|
||||||
break;
|
break;
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
};
|
}, [dispatch, projectId, tab]);
|
||||||
|
|
||||||
const handleSubscribe = () => {
|
// Optimized subscription handler with proper cleanup
|
||||||
|
const handleSubscribe = useCallback(() => {
|
||||||
if (!selectedProject?.id || !socket || subscriptionLoading) return;
|
if (!selectedProject?.id || !socket || subscriptionLoading) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setSubscriptionLoading(true);
|
setSubscriptionLoading(true);
|
||||||
const newSubscriptionState = !selectedProject.subscribed;
|
const newSubscriptionState = !selectedProject.subscribed;
|
||||||
|
|
||||||
// Emit socket event first, then update state based on response
|
// Clear any existing timeout
|
||||||
|
if (subscriptionTimeoutRef.current) {
|
||||||
|
clearTimeout(subscriptionTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit socket event
|
||||||
socket.emit(SocketEvents.PROJECT_SUBSCRIBERS_CHANGE.toString(), {
|
socket.emit(SocketEvents.PROJECT_SUBSCRIBERS_CHANGE.toString(), {
|
||||||
project_id: selectedProject.id,
|
project_id: selectedProject.id,
|
||||||
user_id: currentSession?.id,
|
user_id: currentSession?.id,
|
||||||
team_member_id: currentSession?.team_member_id,
|
team_member_id: currentSession?.team_member_id,
|
||||||
mode: newSubscriptionState ? 0 : 1, // Fixed: 0 for subscribe, 1 for unsubscribe
|
mode: newSubscriptionState ? 0 : 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for the response to confirm the operation
|
// Listen for response with cleanup
|
||||||
socket.once(SocketEvents.PROJECT_SUBSCRIBERS_CHANGE.toString(), (response) => {
|
const handleResponse = (response: any) => {
|
||||||
try {
|
try {
|
||||||
// Update the project state with the confirmed subscription status
|
|
||||||
dispatch(setProject({
|
dispatch(setProject({
|
||||||
...selectedProject,
|
...selectedProject,
|
||||||
subscribed: newSubscriptionState
|
subscribed: newSubscriptionState
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error handling project subscription response:', error);
|
logger.error('Error handling project subscription response:', error);
|
||||||
// Revert optimistic update on error
|
|
||||||
dispatch(setProject({
|
dispatch(setProject({
|
||||||
...selectedProject,
|
...selectedProject,
|
||||||
subscribed: selectedProject.subscribed
|
subscribed: selectedProject.subscribed
|
||||||
}));
|
}));
|
||||||
} finally {
|
} finally {
|
||||||
setSubscriptionLoading(false);
|
setSubscriptionLoading(false);
|
||||||
|
if (subscriptionTimeoutRef.current) {
|
||||||
|
clearTimeout(subscriptionTimeoutRef.current);
|
||||||
|
subscriptionTimeoutRef.current = null;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Add timeout in case socket response never comes
|
socket.once(SocketEvents.PROJECT_SUBSCRIBERS_CHANGE.toString(), handleResponse);
|
||||||
setTimeout(() => {
|
|
||||||
if (subscriptionLoading) {
|
// Set timeout with ref tracking
|
||||||
|
subscriptionTimeoutRef.current = setTimeout(() => {
|
||||||
setSubscriptionLoading(false);
|
setSubscriptionLoading(false);
|
||||||
logger.error('Project subscription timeout - no response from server');
|
logger.error('Project subscription timeout - no response from server');
|
||||||
}
|
subscriptionTimeoutRef.current = null;
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error updating project subscription:', error);
|
logger.error('Error updating project subscription:', error);
|
||||||
setSubscriptionLoading(false);
|
setSubscriptionLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [selectedProject, socket, subscriptionLoading, currentSession, dispatch]);
|
||||||
|
|
||||||
const handleSettingsClick = () => {
|
// Memoized settings handler
|
||||||
|
const handleSettingsClick = useCallback(() => {
|
||||||
if (selectedProject?.id) {
|
if (selectedProject?.id) {
|
||||||
dispatch(setProjectId(selectedProject.id));
|
dispatch(setProjectId(selectedProject.id));
|
||||||
dispatch(fetchProjectData(selectedProject.id));
|
dispatch(fetchProjectData(selectedProject.id));
|
||||||
dispatch(toggleProjectDrawer());
|
dispatch(toggleProjectDrawer());
|
||||||
}
|
}
|
||||||
};
|
}, [dispatch, selectedProject?.id]);
|
||||||
|
|
||||||
|
// Optimized task creation handler
|
||||||
|
const handleCreateTask = useCallback(() => {
|
||||||
|
if (!selectedProject?.id || !currentSession?.id || !socket) return;
|
||||||
|
|
||||||
const handleCreateTask = () => {
|
|
||||||
try {
|
try {
|
||||||
setCreatingTask(true);
|
setCreatingTask(true);
|
||||||
|
|
||||||
const body: Partial<ITaskCreateRequest> = {
|
const body: Partial<ITaskCreateRequest> = {
|
||||||
name: DEFAULT_TASK_NAME,
|
name: DEFAULT_TASK_NAME,
|
||||||
project_id: selectedProject?.id,
|
project_id: selectedProject.id,
|
||||||
reporter_id: currentSession?.id,
|
reporter_id: currentSession.id,
|
||||||
team_id: currentSession?.team_id,
|
team_id: currentSession.team_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
socket?.once(SocketEvents.QUICK_TASK.toString(), (task: IProjectTask) => {
|
const handleTaskCreated = (task: IProjectTask) => {
|
||||||
if (task.id) {
|
if (task.id) {
|
||||||
dispatch(setSelectedTaskId(task.id));
|
dispatch(setSelectedTaskId(task.id));
|
||||||
dispatch(setShowTaskDrawer(true));
|
dispatch(setShowTaskDrawer(true));
|
||||||
@@ -188,100 +200,155 @@ const ProjectViewHeader = () => {
|
|||||||
} else {
|
} else {
|
||||||
dispatch(addTask({ task, groupId }));
|
dispatch(addTask({ task, groupId }));
|
||||||
}
|
}
|
||||||
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id);
|
socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
setCreatingTask(false);
|
||||||
socket?.emit(SocketEvents.QUICK_TASK.toString(), JSON.stringify(body));
|
};
|
||||||
|
|
||||||
|
socket.once(SocketEvents.QUICK_TASK.toString(), handleTaskCreated);
|
||||||
|
socket.emit(SocketEvents.QUICK_TASK.toString(), JSON.stringify(body));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error creating task', error);
|
logger.error('Error creating task', error);
|
||||||
} finally {
|
|
||||||
setCreatingTask(false);
|
setCreatingTask(false);
|
||||||
}
|
}
|
||||||
};
|
}, [selectedProject?.id, currentSession, socket, dispatch, groupBy, tab]);
|
||||||
|
|
||||||
const handleImportTaskTemplate = () => {
|
// Memoized import task template handler
|
||||||
|
const handleImportTaskTemplate = useCallback(() => {
|
||||||
dispatch(setImportTaskTemplateDrawerOpen(true));
|
dispatch(setImportTaskTemplateDrawerOpen(true));
|
||||||
};
|
}, [dispatch]);
|
||||||
|
|
||||||
const dropdownItems = [
|
// 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',
|
key: 'import',
|
||||||
label: (
|
label: (
|
||||||
<div style={{ width: '100%', margin: 0, padding: 0 }} onClick={handleImportTaskTemplate}>
|
<div style={{ width: '100%', margin: 0, padding: 0 }} onClick={handleImportTaskTemplate}>
|
||||||
<ImportOutlined /> Import task
|
<ImportOutlined /> {t('importTask')}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
], [handleImportTaskTemplate, t]);
|
||||||
|
|
||||||
const renderProjectAttributes = () => (
|
// Memoized project attributes with optimized date formatting
|
||||||
<Flex gap={8} align="center">
|
const projectAttributes = useMemo(() => {
|
||||||
{selectedProject?.category_id && (
|
if (!selectedProject) return null;
|
||||||
<Tag color={colors.vibrantOrange} style={{ borderRadius: 24, paddingInline: 8, margin: 0 }}>
|
|
||||||
|
const elements = [];
|
||||||
|
|
||||||
|
if (selectedProject.category_id) {
|
||||||
|
elements.push(
|
||||||
|
<Tag
|
||||||
|
key="category"
|
||||||
|
color={colors.vibrantOrange}
|
||||||
|
style={{ borderRadius: 24, paddingInline: 8, margin: 0 }}
|
||||||
|
>
|
||||||
{selectedProject.category_name}
|
{selectedProject.category_name}
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
{selectedProject?.status && (
|
if (selectedProject.status) {
|
||||||
<Tooltip title={selectedProject.status}>
|
elements.push(
|
||||||
|
<Tooltip key="status" title={selectedProject.status}>
|
||||||
<ProjectStatusIcon
|
<ProjectStatusIcon
|
||||||
iconName={selectedProject.status_icon || ''}
|
iconName={selectedProject.status_icon || ''}
|
||||||
color={selectedProject.status_color || ''}
|
color={selectedProject.status_color || ''}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
{(selectedProject?.start_date || selectedProject?.end_date) && (
|
if (selectedProject.start_date || selectedProject.end_date) {
|
||||||
<Tooltip
|
const tooltipContent = (
|
||||||
title={
|
|
||||||
<Typography.Text style={{ color: colors.white }}>
|
<Typography.Text style={{ color: colors.white }}>
|
||||||
{selectedProject?.start_date &&
|
{selectedProject.start_date &&
|
||||||
`${t('startDate')}: ${formatDate(new Date(selectedProject.start_date))}`}
|
`${t('startDate')}: ${formatDate(new Date(selectedProject.start_date))}`}
|
||||||
{selectedProject?.end_date && (
|
{selectedProject.end_date && (
|
||||||
<>
|
<>
|
||||||
<br />
|
<br />
|
||||||
{`${t('endDate')}: ${formatDate(new Date(selectedProject.end_date))}`}
|
{`${t('endDate')}: ${formatDate(new Date(selectedProject.end_date))}`}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
}
|
|
||||||
>
|
|
||||||
<CalendarOutlined style={{ fontSize: 16 }} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedProject?.notes && (
|
|
||||||
<Typography.Text type="secondary">{selectedProject.notes}</Typography.Text>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderHeaderActions = () => (
|
elements.push(
|
||||||
|
<Tooltip key="dates" title={tooltipContent}>
|
||||||
|
<CalendarOutlined style={{ fontSize: 16 }} />
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedProject.notes) {
|
||||||
|
elements.push(
|
||||||
|
<Typography.Text key="notes" type="secondary">
|
||||||
|
{selectedProject.notes}
|
||||||
|
</Typography.Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<Flex gap={8} align="center">
|
<Flex gap={8} align="center">
|
||||||
<Tooltip title="Refresh project">
|
{elements}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}, [selectedProject, t]);
|
||||||
|
|
||||||
|
// Memoized header actions with conditional rendering optimization
|
||||||
|
const headerActions = useMemo(() => {
|
||||||
|
const actions = [];
|
||||||
|
|
||||||
|
// Refresh button
|
||||||
|
actions.push(
|
||||||
|
<Tooltip key="refresh" title={t('refreshProject')}>
|
||||||
<Button
|
<Button
|
||||||
shape="circle"
|
shape="circle"
|
||||||
icon={<SyncOutlined spin={loadingGroups} />}
|
icon={<SyncOutlined spin={loadingGroups} />}
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
{(isOwnerOrAdmin) && (
|
// Save as template (owner/admin only)
|
||||||
<Tooltip title="Save as template">
|
if (isOwnerOrAdmin) {
|
||||||
|
actions.push(
|
||||||
|
<Tooltip key="template" title={t('saveAsTemplate')}>
|
||||||
<Button
|
<Button
|
||||||
shape="circle"
|
shape="circle"
|
||||||
icon={<SaveOutlined />}
|
icon={<SaveOutlined />}
|
||||||
onClick={() => dispatch(toggleSaveAsTemplateDrawer())}
|
onClick={handleSaveAsTemplate}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<Tooltip title="Project settings">
|
// Settings button
|
||||||
|
actions.push(
|
||||||
|
<Tooltip key="settings" title={t('projectSettings')}>
|
||||||
<Button shape="circle" icon={<SettingOutlined />} onClick={handleSettingsClick} />
|
<Button shape="circle" icon={<SettingOutlined />} onClick={handleSettingsClick} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
<Tooltip title={t('subscribe')}>
|
// Subscribe button
|
||||||
|
actions.push(
|
||||||
|
<Tooltip key="subscribe" title={t('subscribe')}>
|
||||||
<Button
|
<Button
|
||||||
shape="round"
|
shape="round"
|
||||||
loading={subscriptionLoading}
|
loading={subscriptionLoading}
|
||||||
@@ -291,19 +358,27 @@ const ProjectViewHeader = () => {
|
|||||||
{selectedProject?.subscribed ? t('unsubscribe') : t('subscribe')}
|
{selectedProject?.subscribed ? t('unsubscribe') : t('subscribe')}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
{(isOwnerOrAdmin || isProjectManager) && (
|
// Invite button (owner/admin/project manager only)
|
||||||
|
if (isOwnerOrAdmin || isProjectManager) {
|
||||||
|
actions.push(
|
||||||
<Button
|
<Button
|
||||||
|
key="invite"
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<UsergroupAddOutlined />}
|
icon={<UsergroupAddOutlined />}
|
||||||
onClick={() => dispatch(toggleProjectMemberDrawer())}
|
onClick={handleInvite}
|
||||||
>
|
>
|
||||||
Invite
|
{t('invite')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
{isOwnerOrAdmin ? (
|
// Create task button
|
||||||
|
if (isOwnerOrAdmin) {
|
||||||
|
actions.push(
|
||||||
<Dropdown.Button
|
<Dropdown.Button
|
||||||
|
key="create-task-dropdown"
|
||||||
loading={creatingTask}
|
loading={creatingTask}
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<DownOutlined />}
|
icon={<DownOutlined />}
|
||||||
@@ -313,8 +388,11 @@ const ProjectViewHeader = () => {
|
|||||||
>
|
>
|
||||||
<EditOutlined /> {t('createTask')}
|
<EditOutlined /> {t('createTask')}
|
||||||
</Dropdown.Button>
|
</Dropdown.Button>
|
||||||
) : (
|
);
|
||||||
|
} else {
|
||||||
|
actions.push(
|
||||||
<Button
|
<Button
|
||||||
|
key="create-task"
|
||||||
loading={creatingTask}
|
loading={creatingTask}
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
@@ -322,35 +400,75 @@ const ProjectViewHeader = () => {
|
|||||||
>
|
>
|
||||||
{t('createTask')}
|
{t('createTask')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex gap={8} align="center">
|
||||||
|
{actions}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
}, [
|
||||||
|
loadingGroups,
|
||||||
|
handleRefresh,
|
||||||
|
isOwnerOrAdmin,
|
||||||
|
handleSaveAsTemplate,
|
||||||
|
handleSettingsClick,
|
||||||
|
t,
|
||||||
|
subscriptionLoading,
|
||||||
|
selectedProject?.subscribed,
|
||||||
|
handleSubscribe,
|
||||||
|
isProjectManager,
|
||||||
|
handleInvite,
|
||||||
|
creatingTask,
|
||||||
|
dropdownItems,
|
||||||
|
handleCreateTask,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Memoized page header title
|
||||||
|
const pageHeaderTitle = useMemo(() => (
|
||||||
|
<Flex gap={8} align="center">
|
||||||
|
<ArrowLeftOutlined
|
||||||
|
style={{ fontSize: 16 }}
|
||||||
|
onClick={handleNavigateToProjects}
|
||||||
|
/>
|
||||||
|
<Typography.Title level={4} style={{ marginBlockEnd: 0, marginInlineStart: 12 }}>
|
||||||
|
{selectedProject?.name}
|
||||||
|
</Typography.Title>
|
||||||
|
{projectAttributes}
|
||||||
|
</Flex>
|
||||||
|
), [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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
className="site-page-header"
|
className="site-page-header"
|
||||||
title={
|
title={pageHeaderTitle}
|
||||||
<Flex gap={8} align="center">
|
style={pageHeaderStyle}
|
||||||
<ArrowLeftOutlined
|
extra={headerActions}
|
||||||
style={{ fontSize: 16 }}
|
|
||||||
onClick={() => navigate('/worklenz/projects')}
|
|
||||||
/>
|
/>
|
||||||
<Typography.Title level={4} style={{ marginBlockEnd: 0, marginInlineStart: 12 }}>
|
{createPortal(<ProjectDrawer onClose={() => {}} />, document.body, 'project-drawer')}
|
||||||
{selectedProject?.name}
|
|
||||||
</Typography.Title>
|
|
||||||
{renderProjectAttributes()}
|
|
||||||
</Flex>
|
|
||||||
}
|
|
||||||
style={{ paddingInline: 0, marginBlockEnd: 12 }}
|
|
||||||
extra={renderHeaderActions()}
|
|
||||||
/>
|
|
||||||
{createPortal(<ProjectDrawer onClose={() => { }} />, document.body, 'project-drawer')}
|
|
||||||
{createPortal(<ImportTaskTemplate />, document.body, 'import-task-template')}
|
{createPortal(<ImportTaskTemplate />, document.body, 'import-task-template')}
|
||||||
{createPortal(<SaveProjectAsTemplate />, document.body, 'save-project-as-template')}
|
{createPortal(<SaveProjectAsTemplate />, document.body, 'save-project-as-template')}
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ProjectViewHeader.displayName = 'ProjectViewHeader';
|
||||||
|
|
||||||
export default ProjectViewHeader;
|
export default ProjectViewHeader;
|
||||||
|
|||||||
Reference in New Issue
Block a user