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:
Chamika J
2025-06-27 13:24:43 +05:30
committed by GitHub
6 changed files with 262 additions and 124 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
setSubscriptionLoading(false); subscriptionTimeoutRef.current = setTimeout(() => {
logger.error('Project subscription timeout - no response from server'); setSubscriptionLoading(false);
} 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> );
}
> elements.push(
<Tooltip key="dates" title={tooltipContent}>
<CalendarOutlined style={{ fontSize: 16 }} /> <CalendarOutlined style={{ fontSize: 16 }} />
</Tooltip> </Tooltip>
)} );
}
{selectedProject?.notes && ( if (selectedProject.notes) {
<Typography.Text type="secondary">{selectedProject.notes}</Typography.Text> elements.push(
)} <Typography.Text key="notes" type="secondary">
</Flex> {selectedProject.notes}
); </Typography.Text>
);
}
const renderHeaderActions = () => ( 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>
);
}, [
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> </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 }}>
{selectedProject?.name}
</Typography.Title>
{renderProjectAttributes()}
</Flex>
}
style={{ paddingInline: 0, marginBlockEnd: 12 }}
extra={renderHeaderActions()}
/> />
{createPortal(<ProjectDrawer onClose={() => { }} />, document.body, 'project-drawer')} {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;