Files
worklenz/worklenz-frontend/src/components/task-drawer/shared/info-tab/task-drawer-info-tab.tsx
chamiakJ 6128c64c31 Add task progress tracking methods and enhance UI components
- Introduced a comprehensive guide for users on task progress tracking methods, including manual, weighted, and time-based progress.
- Implemented backend support for progress calculations, including SQL functions and migrations to accommodate new progress features.
- Enhanced frontend components to support progress input and display, including updates to task and project drawers.
- Added localization for new progress-related terms and validation messages.
- Integrated real-time updates for task progress and weight changes through socket events.
2025-04-30 15:24:07 +05:30

292 lines
9.4 KiB
TypeScript

import { Button, Collapse, CollapseProps, Flex, Skeleton, Tooltip, Typography, Upload } from 'antd';
import React, { useEffect, useState, useRef } from 'react';
import { ReloadOutlined } from '@ant-design/icons';
import DescriptionEditor from './description-editor';
import SubTaskTable from './subtask-table';
import DependenciesTable from './dependencies-table';
import { useAppSelector } from '@/hooks/useAppSelector';
import TaskDetailsForm from './task-details-form';
import { fetchTask } from '@/features/tasks/tasks.slice';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { TFunction } from 'i18next';
import { subTasksApiService } from '@/api/tasks/subtasks.api.service';
import { ISubTask } from '@/types/tasks/subTask.types';
import { ITaskDependency } from '@/types/tasks/task-dependency.types';
import { taskDependenciesApiService } from '@/api/tasks/task-dependencies.api.service';
import logger from '@/utils/errorLogger';
import { getBase64 } from '@/utils/file-utils';
import {
ITaskAttachment,
ITaskAttachmentViewModel,
} from '@/types/tasks/task-attachment-view-model';
import taskAttachmentsApiService from '@/api/tasks/task-attachments.api.service';
import AttachmentsGrid from './attachments/attachments-grid';
import TaskComments from './comments/task-comments';
import { ITaskCommentViewModel } from '@/types/tasks/task-comments.types';
import taskCommentsApiService from '@/api/tasks/task-comments.api.service';
interface TaskDrawerInfoTabProps {
t: TFunction;
}
const TaskDrawerInfoTab = ({ t }: TaskDrawerInfoTabProps) => {
const dispatch = useAppDispatch();
const { projectId } = useAppSelector(state => state.projectReducer);
const { taskFormViewModel, loadingTask, selectedTaskId } = useAppSelector(
state => state.taskDrawerReducer
);
const [subTasks, setSubTasks] = useState<ISubTask[]>([]);
const [loadingSubTasks, setLoadingSubTasks] = useState<boolean>(false);
const [taskDependencies, setTaskDependencies] = useState<ITaskDependency[]>([]);
const [loadingTaskDependencies, setLoadingTaskDependencies] = useState<boolean>(false);
const [processingUpload, setProcessingUpload] = useState(false);
const selectedFilesRef = useRef<File[]>([]);
const [taskAttachments, setTaskAttachments] = useState<ITaskAttachmentViewModel[]>([]);
const [loadingTaskAttachments, setLoadingTaskAttachments] = useState<boolean>(false);
const [taskComments, setTaskComments] = useState<ITaskCommentViewModel[]>([]);
const [loadingTaskComments, setLoadingTaskComments] = useState<boolean>(false);
const handleFilesSelected = async (files: File[]) => {
if (!taskFormViewModel?.task?.id || !projectId) return;
if (!processingUpload) {
setProcessingUpload(true);
try {
const filesToUpload = [...files];
selectedFilesRef.current = filesToUpload;
// Upload all files and wait for all promises to complete
await Promise.all(
filesToUpload.map(async file => {
const base64 = await getBase64(file);
const body: ITaskAttachment = {
file: base64 as string,
file_name: file.name,
task_id: taskFormViewModel?.task?.id || '',
project_id: projectId,
size: file.size,
};
await taskAttachmentsApiService.createTaskAttachment(body);
})
);
} finally {
setProcessingUpload(false);
selectedFilesRef.current = [];
// Refetch attachments after all uploads are complete
fetchTaskAttachments();
}
}
};
const fetchTaskData = () => {
if (!loadingTask && selectedTaskId && projectId) {
dispatch(fetchTask({ taskId: selectedTaskId, projectId }));
}
};
const panelStyle: React.CSSProperties = {
border: 'none',
paddingBlock: 0,
};
// Define all info items
const allInfoItems: CollapseProps['items'] = [
{
key: 'details',
label: <Typography.Text strong>{t('taskInfoTab.details.title')}</Typography.Text>,
children: <TaskDetailsForm taskFormViewModel={taskFormViewModel} />,
style: panelStyle,
className: 'custom-task-drawer-info-collapse',
},
{
key: 'description',
label: <Typography.Text strong>{t('taskInfoTab.description.title')}</Typography.Text>,
children: (
<DescriptionEditor
description={taskFormViewModel?.task?.description || null}
taskId={taskFormViewModel?.task?.id || ''}
parentTaskId={taskFormViewModel?.task?.parent_task_id || ''}
/>
),
style: panelStyle,
className: 'custom-task-drawer-info-collapse',
},
{
key: 'subTasks',
label: <Typography.Text strong>{t('taskInfoTab.subTasks.title')}</Typography.Text>,
extra: (
<Tooltip title={t('taskInfoTab.subTasks.refreshSubTasks')} trigger={'hover'}>
<Button
shape="circle"
icon={<ReloadOutlined spin={loadingSubTasks} />}
onClick={e => {
e.stopPropagation(); // Prevent click from bubbling up
fetchSubTasks();
}}
/>
</Tooltip>
),
children: (
<SubTaskTable
subTasks={subTasks}
loadingSubTasks={loadingSubTasks}
refreshSubTasks={() => fetchSubTasks()}
t={t}
/>
),
style: panelStyle,
className: 'custom-task-drawer-info-collapse',
},
{
key: 'dependencies',
label: <Typography.Text strong>{t('taskInfoTab.dependencies.title')}</Typography.Text>,
children: (
<DependenciesTable
task={taskFormViewModel?.task || {}}
t={t}
taskDependencies={taskDependencies}
loadingTaskDependencies={loadingTaskDependencies}
refreshTaskDependencies={() => fetchTaskDependencies()}
/>
),
style: panelStyle,
className: 'custom-task-drawer-info-collapse',
},
{
key: 'attachments',
label: <Typography.Text strong>{t('taskInfoTab.attachments.title')}</Typography.Text>,
children: (
<Flex vertical gap={16}>
<AttachmentsGrid
attachments={taskAttachments}
onDelete={() => fetchTaskAttachments()}
onUpload={() => fetchTaskAttachments()}
t={t}
loadingTask={loadingTask}
uploading={processingUpload}
handleFilesSelected={handleFilesSelected}
/>
</Flex>
),
style: panelStyle,
className: 'custom-task-drawer-info-collapse',
},
{
key: 'comments',
label: <Typography.Text strong>{t('taskInfoTab.comments.title')}</Typography.Text>,
style: panelStyle,
className: 'custom-task-drawer-info-collapse',
children: <TaskComments taskId={selectedTaskId || ''} t={t} />,
},
];
// Filter out the 'subTasks' item if this task is more than level 2
const infoItems =
(taskFormViewModel?.task?.task_level ?? 0) >= 2
? allInfoItems.filter(item => item.key !== 'subTasks')
: allInfoItems;
const fetchSubTasks = async () => {
if (!selectedTaskId || loadingSubTasks) return;
try {
setLoadingSubTasks(true);
const res = await subTasksApiService.getSubTasks(selectedTaskId);
if (res.done) {
setSubTasks(res.body);
}
} catch (error) {
logger.error('Error fetching sub tasks:', error);
} finally {
setLoadingSubTasks(false);
}
};
const fetchTaskDependencies = async () => {
if (!selectedTaskId || loadingTaskDependencies) return;
try {
setLoadingTaskDependencies(true);
const res = await taskDependenciesApiService.getTaskDependencies(selectedTaskId);
if (res.done) {
setTaskDependencies(res.body);
}
} catch (error) {
logger.error('Error fetching task dependencies:', error);
} finally {
setLoadingTaskDependencies(false);
}
};
const fetchTaskAttachments = async () => {
if (!selectedTaskId || loadingTaskAttachments) return;
try {
setLoadingTaskAttachments(true);
const res = await taskAttachmentsApiService.getTaskAttachments(selectedTaskId);
if (res.done) {
setTaskAttachments(res.body);
}
} catch (error) {
logger.error('Error fetching task attachments:', error);
} finally {
setLoadingTaskAttachments(false);
}
};
const fetchTaskComments = async () => {
if (!selectedTaskId || loadingTaskComments) return;
try {
setLoadingTaskComments(true);
const res = await taskCommentsApiService.getByTaskId(selectedTaskId);
if (res.done) {
setTaskComments(res.body);
}
} catch (error) {
logger.error('Error fetching task comments:', error);
} finally {
setLoadingTaskComments(false);
}
};
useEffect(() => {
fetchTaskData();
fetchSubTasks();
fetchTaskDependencies();
fetchTaskAttachments();
fetchTaskComments();
return () => {
setSubTasks([]);
setTaskDependencies([]);
setTaskAttachments([]);
selectedFilesRef.current = [];
setTaskComments([]);
};
}, [selectedTaskId, projectId]);
return (
<Skeleton active loading={loadingTask}>
<Flex vertical>
<Collapse
items={infoItems}
bordered={false}
defaultActiveKey={[
'details',
'description',
'subTasks',
'dependencies',
'attachments',
'comments',
]}
/>
</Flex>
</Skeleton>
);
};
export default TaskDrawerInfoTab;