From f9926e7a5dc27b62b8351514f6f6d8a86bb57311 Mon Sep 17 00:00:00 2001 From: chamiakJ Date: Mon, 7 Jul 2025 07:05:29 +0530 Subject: [PATCH] feat(task-list): add tooltips for task indicators and enhance localization - Introduced tooltips for subtasks, comments, attachments, subscribers, dependencies, and recurring tasks across various components to improve user experience. - Enhanced localization by adding new translation keys for these indicators in multiple languages, ensuring consistent messaging for users. - Updated components such as TaskRow, KanbanTaskCard, and EnhancedKanbanTaskCard to utilize the new tooltip functionality, improving clarity and accessibility. --- .../public/locales/alb/task-list-table.json | 14 +++ .../public/locales/de/task-list-table.json | 14 +++ .../public/locales/en/task-list-table.json | 14 +++ .../public/locales/en/task-management.json | 16 +++- .../public/locales/es/task-list-table.json | 14 +++ .../public/locales/pt/task-list-table.json | 14 +++ .../EnhancedKanbanTaskCard.tsx | 40 +++++---- .../kanbanTaskCard.tsx | 18 ++-- .../attachments/attachments-preview.tsx | 5 ++ .../info-tab/comments/task-comments.tsx | 25 +++++- .../comments/task-view-comment-edit.tsx | 7 +- .../shared/info-tab/dependencies-table.tsx | 21 +++++ .../task-drawer-recurring-config.tsx | 9 ++ .../shared/info-tab/info-tab-footer.tsx | 9 +- .../info-tab/notify-member-selector.tsx | 11 ++- .../shared/info-tab/task-drawer-info-tab.tsx | 20 ++++- .../src/components/task-list-v2/TaskRow.tsx | 64 ++++++++------ .../task-management/improved-task-filters.tsx | 87 ------------------- .../components/task-management/task-row.tsx | 10 +-- .../task-management/task-management.slice.ts | 33 +++++++ .../task-management/taskListFields.slice.ts | 4 +- .../src/hooks/useTaskSocketHandlers.ts | 13 ++- 22 files changed, 304 insertions(+), 158 deletions(-) diff --git a/worklenz-frontend/public/locales/alb/task-list-table.json b/worklenz-frontend/public/locales/alb/task-list-table.json index 2c008793..067d1088 100644 --- a/worklenz-frontend/public/locales/alb/task-list-table.json +++ b/worklenz-frontend/public/locales/alb/task-list-table.json @@ -108,5 +108,19 @@ "key": "Çelësi", "formula": "Formula" } + }, + + "indicators": { + "tooltips": { + "subtasks": "{{count}} nën-detyrë", + "subtasks_plural": "{{count}} nën-detyra", + "comments": "{{count}} koment", + "comments_plural": "{{count}} komente", + "attachments": "{{count}} bashkëngjitje", + "attachments_plural": "{{count}} bashkëngjitje", + "subscribers": "Detyra ka pajtues", + "dependencies": "Detyra ka varësi", + "recurring": "Detyrë përsëritëse" + } } } diff --git a/worklenz-frontend/public/locales/de/task-list-table.json b/worklenz-frontend/public/locales/de/task-list-table.json index b9bcd10e..fa8e7623 100644 --- a/worklenz-frontend/public/locales/de/task-list-table.json +++ b/worklenz-frontend/public/locales/de/task-list-table.json @@ -108,5 +108,19 @@ "key": "Schlüssel", "formula": "Formel" } + }, + + "indicators": { + "tooltips": { + "subtasks": "{{count}} Unteraufgabe", + "subtasks_plural": "{{count}} Unteraufgaben", + "comments": "{{count}} Kommentar", + "comments_plural": "{{count}} Kommentare", + "attachments": "{{count}} Anhang", + "attachments_plural": "{{count}} Anhänge", + "subscribers": "Aufgabe hat Abonnenten", + "dependencies": "Aufgabe hat Abhängigkeiten", + "recurring": "Wiederkehrende Aufgabe" + } } } diff --git a/worklenz-frontend/public/locales/en/task-list-table.json b/worklenz-frontend/public/locales/en/task-list-table.json index b6543d30..674f12d0 100644 --- a/worklenz-frontend/public/locales/en/task-list-table.json +++ b/worklenz-frontend/public/locales/en/task-list-table.json @@ -108,5 +108,19 @@ "key": "Key", "formula": "Formula" } + }, + + "indicators": { + "tooltips": { + "subtasks": "{{count}} subtask", + "subtasks_plural": "{{count}} subtasks", + "comments": "{{count}} comment", + "comments_plural": "{{count}} comments", + "attachments": "{{count}} attachment", + "attachments_plural": "{{count}} attachments", + "subscribers": "Task has subscribers", + "dependencies": "Task has dependencies", + "recurring": "Recurring task" + } } } diff --git a/worklenz-frontend/public/locales/en/task-management.json b/worklenz-frontend/public/locales/en/task-management.json index 662d5081..2d21c746 100644 --- a/worklenz-frontend/public/locales/en/task-management.json +++ b/worklenz-frontend/public/locales/en/task-management.json @@ -17,5 +17,19 @@ "renamePhase": "Rename Phase", "changeCategory": "Change Category", "clickToEditGroupName": "Click to edit group name", - "enterGroupName": "Enter group name" + "enterGroupName": "Enter group name", + + "indicators": { + "tooltips": { + "subtasks": "{{count}} subtask", + "subtasks_plural": "{{count}} subtasks", + "comments": "{{count}} comment", + "comments_plural": "{{count}} comments", + "attachments": "{{count}} attachment", + "attachments_plural": "{{count}} attachments", + "subscribers": "Task has subscribers", + "dependencies": "Task has dependencies", + "recurring": "Recurring task" + } + } } diff --git a/worklenz-frontend/public/locales/es/task-list-table.json b/worklenz-frontend/public/locales/es/task-list-table.json index edd9cc0a..006a2763 100644 --- a/worklenz-frontend/public/locales/es/task-list-table.json +++ b/worklenz-frontend/public/locales/es/task-list-table.json @@ -108,5 +108,19 @@ "key": "Clave", "formula": "Fórmula" } + }, + + "indicators": { + "tooltips": { + "subtasks": "{{count}} subtarea", + "subtasks_plural": "{{count}} subtareas", + "comments": "{{count}} comentario", + "comments_plural": "{{count}} comentarios", + "attachments": "{{count}} archivo adjunto", + "attachments_plural": "{{count}} archivos adjuntos", + "subscribers": "La tarea tiene suscriptores", + "dependencies": "La tarea tiene dependencias", + "recurring": "Tarea recurrente" + } } } diff --git a/worklenz-frontend/public/locales/pt/task-list-table.json b/worklenz-frontend/public/locales/pt/task-list-table.json index bd6d3cb7..a493fcf0 100644 --- a/worklenz-frontend/public/locales/pt/task-list-table.json +++ b/worklenz-frontend/public/locales/pt/task-list-table.json @@ -108,5 +108,19 @@ "key": "Chave", "formula": "Fórmula" } + }, + + "indicators": { + "tooltips": { + "subtasks": "{{count}} subtarefa", + "subtasks_plural": "{{count}} subtarefas", + "comments": "{{count}} comentário", + "comments_plural": "{{count}} comentários", + "attachments": "{{count}} anexo", + "attachments_plural": "{{count}} anexos", + "subscribers": "A tarefa tem assinantes", + "dependencies": "A tarefa tem dependências", + "recurring": "Tarefa recorrente" + } } } diff --git a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.tsx b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.tsx index 934fb365..481d507c 100644 --- a/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.tsx +++ b/worklenz-frontend/src/components/enhanced-kanban/EnhancedKanbanTaskCard.tsx @@ -193,28 +193,30 @@ const EnhancedKanbanTaskCard: React.FC = React.memo {/* Subtask Section - only show if count > 1 */} {task.sub_tasks_count != null && Number(task.sub_tasks_count) > 1 && ( - + + + {task.sub_tasks_count || 0} + {task.show_sub_tasks ? : } + + + )} diff --git a/worklenz-frontend/src/components/kanban-board-management-v2/kanbanTaskCard.tsx b/worklenz-frontend/src/components/kanban-board-management-v2/kanbanTaskCard.tsx index be35f8c7..766fbfce 100644 --- a/worklenz-frontend/src/components/kanban-board-management-v2/kanbanTaskCard.tsx +++ b/worklenz-frontend/src/components/kanban-board-management-v2/kanbanTaskCard.tsx @@ -10,6 +10,7 @@ import { } from '@ant-design/icons'; import { IProjectTask } from '@/types/project/projectTasksViewModel.types'; import { IGroupBy } from '@/features/tasks/tasks.slice'; +import { useTranslation } from 'react-i18next'; const { Text } = Typography; @@ -36,6 +37,7 @@ const KanbanTaskCard: React.FC = ({ onSelect, onToggleSubtasks, }) => { + const { t } = useTranslation('task-list-table'); const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: task.id!, data: { @@ -196,14 +198,18 @@ const KanbanTaskCard: React.FC = ({ )} {task.comments_count && task.comments_count > 1 && ( - - {task.comments_count} - + + + {task.comments_count} + + )} {task.attachments_count && task.attachments_count > 1 && ( - - {task.attachments_count} - + + + {task.attachments_count} + + )} diff --git a/worklenz-frontend/src/components/task-drawer/shared/info-tab/attachments/attachments-preview.tsx b/worklenz-frontend/src/components/task-drawer/shared/info-tab/attachments/attachments-preview.tsx index 83f328b6..ad9b8aba 100644 --- a/worklenz-frontend/src/components/task-drawer/shared/info-tab/attachments/attachments-preview.tsx +++ b/worklenz-frontend/src/components/task-drawer/shared/info-tab/attachments/attachments-preview.tsx @@ -15,6 +15,8 @@ import taskAttachmentsApiService from '@/api/tasks/task-attachments.api.service' import logger from '@/utils/errorLogger'; import taskCommentsApiService from '@/api/tasks/task-comments.api.service'; import { useAppSelector } from '@/hooks/useAppSelector'; +import { useAppDispatch } from '@/hooks/useAppDispatch'; +import { updateTaskCounts } from '@/features/task-management/task-management.slice'; interface AttachmentsPreviewProps { attachment: ITaskAttachmentViewModel; @@ -28,6 +30,7 @@ const AttachmentsPreview = ({ isCommentAttachment = false, }: AttachmentsPreviewProps) => { const { selectedTaskId } = useAppSelector(state => state.taskDrawerReducer); + const dispatch = useAppDispatch(); const [deleting, setDeleting] = useState(false); const [isVisible, setIsVisible] = useState(false); const [currentFileUrl, setCurrentFileUrl] = useState(null); @@ -88,6 +91,7 @@ const AttachmentsPreview = ({ if (isCommentAttachment) { const res = await taskCommentsApiService.deleteAttachment(id, selectedTaskId); if (res.done) { + // Let the parent component handle the refetch and Redux update document.dispatchEvent(new Event('task-comment-update')); } } else { @@ -96,6 +100,7 @@ const AttachmentsPreview = ({ if (onDelete) { onDelete(id); } + // Parent component will handle the refetch and Redux update } } } catch (e) { diff --git a/worklenz-frontend/src/components/task-drawer/shared/info-tab/comments/task-comments.tsx b/worklenz-frontend/src/components/task-drawer/shared/info-tab/comments/task-comments.tsx index d697f4eb..95e636fc 100644 --- a/worklenz-frontend/src/components/task-drawer/shared/info-tab/comments/task-comments.tsx +++ b/worklenz-frontend/src/components/task-drawer/shared/info-tab/comments/task-comments.tsx @@ -13,12 +13,16 @@ import logger from '@/utils/errorLogger'; import TaskViewCommentEdit from './task-view-comment-edit'; import './task-comments.css'; import { useAppSelector } from '@/hooks/useAppSelector'; +import { useAppDispatch } from '@/hooks/useAppDispatch'; +import { updateTaskCounts } from '@/features/task-management/task-management.slice'; import { themeWiseColor } from '@/utils/themeWiseColor'; import { colors } from '@/styles/colors'; import AttachmentsGrid from '../attachments/attachments-grid'; import { TFunction } from 'i18next'; import SingleAvatar from '@/components/common/single-avatar/single-avatar'; import { sanitizeHtml } from '@/utils/sanitizeInput'; +import { useSocket } from '@/socket/socketContext'; +import { SocketEvents } from '@/shared/socket-events'; // Helper function to format date for time separators const formatDateForSeparator = (date: string) => { @@ -90,6 +94,8 @@ const TaskComments = ({ taskId, t }: { taskId?: string; t: TFunction }) => { const auth = useAuthService(); const themeMode = useAppSelector(state => state.themeReducer.mode); const currentUserId = auth.getCurrentSession()?.id; + const { socket, connected } = useSocket(); + const dispatch = useAppDispatch(); const getComments = useCallback( async (showLoading = true) => { @@ -115,6 +121,14 @@ const TaskComments = ({ taskId, t }: { taskId?: string; t: TFunction }) => { }); setComments(sortedComments); + + // Update Redux state with the current comment count + dispatch(updateTaskCounts({ + taskId, + counts: { + comments_count: sortedComments.length + } + })); } setLoading(false); @@ -123,7 +137,7 @@ const TaskComments = ({ taskId, t }: { taskId?: string; t: TFunction }) => { setLoading(false); } }, - [taskId] + [taskId, dispatch] ); useEffect(() => { @@ -196,8 +210,11 @@ const TaskComments = ({ taskId, t }: { taskId?: string; t: TFunction }) => { try { const res = await taskCommentsApiService.delete(id, taskId); if (res.done) { + // Refresh comments to get updated list await getComments(false); - document.dispatchEvent(new Event('task-comment-update')); + + // The comment count will be updated by getComments function + // No need to dispatch here as getComments already handles it } } catch (e) { logger.error('Error deleting comment', e); @@ -227,7 +244,9 @@ const TaskComments = ({ taskId, t }: { taskId?: string; t: TFunction }) => { await getComments(false); // Dispatch event to notify that an attachment was deleted - document.dispatchEvent(new Event('task-comment-update')); + document.dispatchEvent(new CustomEvent('task-comment-update', { + detail: { taskId } + })); } } catch (e) { logger.error('Error deleting attachment', e); diff --git a/worklenz-frontend/src/components/task-drawer/shared/info-tab/comments/task-view-comment-edit.tsx b/worklenz-frontend/src/components/task-drawer/shared/info-tab/comments/task-view-comment-edit.tsx index e8af1b22..e215ad99 100644 --- a/worklenz-frontend/src/components/task-drawer/shared/info-tab/comments/task-view-comment-edit.tsx +++ b/worklenz-frontend/src/components/task-drawer/shared/info-tab/comments/task-view-comment-edit.tsx @@ -6,6 +6,8 @@ import logger from '@/utils/errorLogger'; import { useAppSelector } from '@/hooks/useAppSelector'; import { themeWiseColor } from '@/utils/themeWiseColor'; import { colors } from '@/styles/colors'; +import { useSocket } from '@/socket/socketContext'; +import { SocketEvents } from '@/shared/socket-events'; interface TaskViewCommentEditProps { commentData: ITaskCommentViewModel; @@ -27,6 +29,7 @@ const TaskViewCommentEdit = ({ commentData, onUpdated }: TaskViewCommentEditProp const themeMode = useAppSelector(state => state.themeReducer.mode); const [loading, setLoading] = useState(false); const [content, setContent] = useState(''); + const { socket, connected } = useSocket(); // Initialize content when component mounts useEffect(() => { @@ -55,7 +58,9 @@ const TaskViewCommentEdit = ({ commentData, onUpdated }: TaskViewCommentEditProp onUpdated(commentData); // Dispatch event to notify that a comment was updated - document.dispatchEvent(new Event('task-comment-update')); + document.dispatchEvent(new CustomEvent('task-comment-update', { + detail: { taskId: commentData.task_id } + })); } } catch (e) { logger.error('Error updating comment', e); diff --git a/worklenz-frontend/src/components/task-drawer/shared/info-tab/dependencies-table.tsx b/worklenz-frontend/src/components/task-drawer/shared/info-tab/dependencies-table.tsx index a479a420..28568a38 100644 --- a/worklenz-frontend/src/components/task-drawer/shared/info-tab/dependencies-table.tsx +++ b/worklenz-frontend/src/components/task-drawer/shared/info-tab/dependencies-table.tsx @@ -14,6 +14,8 @@ import { import React, { useState, useEffect } from 'react'; import { DeleteOutlined, ExclamationCircleFilled } from '@ant-design/icons'; import { useAppSelector } from '@/hooks/useAppSelector'; +import { useAppDispatch } from '@/hooks/useAppDispatch'; +import { updateTaskCounts } from '@/features/task-management/task-management.slice'; import { colors } from '@/styles/colors'; import { TFunction } from 'i18next'; import { IDependencyType, ITaskDependency } from '@/types/tasks/task-dependency.types'; @@ -40,6 +42,7 @@ const DependenciesTable = ({ const [hoverRow, setHoverRow] = useState(null); const [isDependencyInputShow, setIsDependencyInputShow] = useState(false); const { projectId } = useAppSelector(state => state.projectReducer); + const dispatch = useAppDispatch(); const [taskList, setTaskList] = useState<{ label: string; value: string }[]>([]); const [loadingTaskList, setLoadingTaskList] = useState(false); const [searchTerm, setSearchTerm] = useState(''); @@ -60,6 +63,14 @@ const DependenciesTable = ({ setIsDependencyInputShow(false); setTaskList([]); setSearchTerm(''); + + // Update Redux state with dependency status + dispatch(updateTaskCounts({ + taskId: task.id, + counts: { + has_dependencies: true + } + })); } } catch (error) { console.error('Error adding dependency:', error); @@ -89,6 +100,16 @@ const DependenciesTable = ({ const res = await taskDependenciesApiService.deleteTaskDependency(dependencyId); if (res.done) { refreshTaskDependencies(); + + // Update Redux state with dependency status + // Check if there are any remaining dependencies + const remainingDependencies = taskDependencies.filter(dep => dep.id !== dependencyId); + dispatch(updateTaskCounts({ + taskId: task.id, + counts: { + has_dependencies: remainingDependencies.length > 0 + } + })); } } catch (error) { console.error('Error deleting dependency:', error); diff --git a/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-recurring-config/task-drawer-recurring-config.tsx b/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-recurring-config/task-drawer-recurring-config.tsx index 21c786b3..1d12c480 100644 --- a/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-recurring-config/task-drawer-recurring-config.tsx +++ b/worklenz-frontend/src/components/task-drawer/shared/info-tab/details/task-drawer-recurring-config/task-drawer-recurring-config.tsx @@ -25,6 +25,7 @@ import { ITaskViewModel } from '@/types/tasks/task.types'; import { useTranslation } from 'react-i18next'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { updateRecurringChange } from '@/features/tasks/tasks.slice'; +import { updateTaskCounts } from '@/features/task-management/task-management.slice'; import { taskRecurringApiService } from '@/api/tasks/task-recurring.api.service'; import logger from '@/utils/errorLogger'; import { setTaskRecurringSchedule } from '@/features/task-drawer/task-drawer.slice'; @@ -100,6 +101,14 @@ const TaskDrawerRecurringConfig = ({ task }: { task: ITaskViewModel }) => { setTaskRecurringSchedule({ schedule_id: schedule.id as string, task_id: task.id }) ); + // Update Redux state with recurring task status + dispatch(updateTaskCounts({ + taskId: task.id, + counts: { + schedule_id: schedule.id as string || null + } + })); + setRecurring(checked); if (!checked) setShowConfig(false); } diff --git a/worklenz-frontend/src/components/task-drawer/shared/info-tab/info-tab-footer.tsx b/worklenz-frontend/src/components/task-drawer/shared/info-tab/info-tab-footer.tsx index 3b8484c3..e63c2a3c 100644 --- a/worklenz-frontend/src/components/task-drawer/shared/info-tab/info-tab-footer.tsx +++ b/worklenz-frontend/src/components/task-drawer/shared/info-tab/info-tab-footer.tsx @@ -2,6 +2,8 @@ import { Button, Flex, Form, Mentions, Space, Tooltip, Typography, message } fro import { useCallback, useEffect, useRef, useState } from 'react'; import { PaperClipOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons'; import { useAppSelector } from '@/hooks/useAppSelector'; +import { useAppDispatch } from '@/hooks/useAppDispatch'; +import { updateTaskCounts } from '@/features/task-management/task-management.slice'; import { colors } from '@/styles/colors'; import { themeWiseColor } from '@/utils/themeWiseColor'; import { formatDateTimeWithLocale } from '@/utils/format-date-time-with-locale'; @@ -47,6 +49,7 @@ const InfoTabFooter = () => { const { taskFormViewModel, selectedTaskId } = useAppSelector(state => state.taskDrawerReducer); const { projectId } = useAppSelector(state => state.projectReducer); + const dispatch = useAppDispatch(); const [members, setMembers] = useState([]); const [membersLoading, setMembersLoading] = useState(false); @@ -156,8 +159,10 @@ const InfoTabFooter = () => { setCommentValue(''); // Dispatch event to notify that a comment was created - // This will trigger scrolling to the new comment - document.dispatchEvent(new Event('task-comment-create')); + // This will trigger the task comments component to refresh and update Redux + document.dispatchEvent(new CustomEvent('task-comment-create', { + detail: { taskId: selectedTaskId } + })); } } catch (error) { logger.error('Failed to create comment:', error); diff --git a/worklenz-frontend/src/components/task-drawer/shared/info-tab/notify-member-selector.tsx b/worklenz-frontend/src/components/task-drawer/shared/info-tab/notify-member-selector.tsx index c3660dd6..cde137d9 100644 --- a/worklenz-frontend/src/components/task-drawer/shared/info-tab/notify-member-selector.tsx +++ b/worklenz-frontend/src/components/task-drawer/shared/info-tab/notify-member-selector.tsx @@ -15,6 +15,8 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { TFunction } from 'i18next'; import { useAppSelector } from '@/hooks/useAppSelector'; +import { useAppDispatch } from '@/hooks/useAppDispatch'; +import { updateTaskCounts } from '@/features/task-management/task-management.slice'; import { ITaskViewModel } from '@/types/tasks/task.types'; import { ITeamMembersViewModel } from '@/types/teamMembers/teamMembersViewModel.types'; import { teamMembersApiService } from '@/api/team-members/teamMembers.api.service'; @@ -27,7 +29,6 @@ import { useAuthService } from '@/hooks/useAuth'; import Avatars from '@/components/avatars/avatars'; import { tasksApiService } from '@/api/tasks/tasks.api.service'; import { setTaskSubscribers } from '@/features/task-drawer/task-drawer.slice'; -import { useAppDispatch } from '@/hooks/useAppDispatch'; import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types'; import useTabSearchParam from '@/hooks/useTabSearchParam'; import { InlineMember } from '@/types/teamMembers/inlineMember.types'; @@ -100,6 +101,14 @@ const NotifyMemberSelector = ({ task, t }: NotifyMemberSelectorProps) => { socket?.emit(SocketEvents.TASK_SUBSCRIBERS_CHANGE.toString(), body); socket?.once(SocketEvents.TASK_SUBSCRIBERS_CHANGE.toString(), (data: InlineMember[]) => { dispatch(setTaskSubscribers(data)); + + // Update Redux state with subscriber status + dispatch(updateTaskCounts({ + taskId: task.id, + counts: { + has_subscribers: data && data.length > 0 + } + })); }); } catch (error) { logger.error('Error notifying member:', error); diff --git a/worklenz-frontend/src/components/task-drawer/shared/info-tab/task-drawer-info-tab.tsx b/worklenz-frontend/src/components/task-drawer/shared/info-tab/task-drawer-info-tab.tsx index e382e052..e90c9cce 100644 --- a/worklenz-frontend/src/components/task-drawer/shared/info-tab/task-drawer-info-tab.tsx +++ b/worklenz-frontend/src/components/task-drawer/shared/info-tab/task-drawer-info-tab.tsx @@ -8,6 +8,7 @@ import { useAppSelector } from '@/hooks/useAppSelector'; import TaskDetailsForm from './task-details-form'; import { fetchTask } from '@/features/tasks/tasks.slice'; import { useAppDispatch } from '@/hooks/useAppDispatch'; +import { updateTaskCounts } from '@/features/task-management/task-management.slice'; import { TFunction } from 'i18next'; import { subTasksApiService } from '@/api/tasks/subtasks.api.service'; import { ISubTask } from '@/types/tasks/subTask.types'; @@ -24,6 +25,7 @@ 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'; +import { ITaskViewModel } from '@/types/tasks/task.types'; interface TaskDrawerInfoTabProps { t: TFunction; @@ -148,7 +150,7 @@ const TaskDrawerInfoTab = ({ t }: TaskDrawerInfoTabProps) => { label: {t('taskInfoTab.dependencies.title')}, children: ( { const res = await taskDependenciesApiService.getTaskDependencies(selectedTaskId); if (res.done) { setTaskDependencies(res.body); + + // Update Redux state with the current dependency status + dispatch(updateTaskCounts({ + taskId: selectedTaskId, + counts: { + has_dependencies: res.body.length > 0 + } + })); } } catch (error) { logger.error('Error fetching task dependencies:', error); @@ -229,6 +239,14 @@ const TaskDrawerInfoTab = ({ t }: TaskDrawerInfoTabProps) => { const res = await taskAttachmentsApiService.getTaskAttachments(selectedTaskId); if (res.done) { setTaskAttachments(res.body); + + // Update Redux state with the current attachment count + dispatch(updateTaskCounts({ + taskId: selectedTaskId, + counts: { + attachments_count: res.body.length + } + })); } } catch (error) { logger.error('Error fetching task attachments:', error); diff --git a/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx b/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx index 30467a66..a816dfe0 100644 --- a/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx +++ b/worklenz-frontend/src/components/task-list-v2/TaskRow.tsx @@ -328,55 +328,65 @@ const TaskRow: React.FC = memo(({ taskId, projectId, visibleColumn {/* Subtask count indicator - only show if count > 1 */} {!isSubtask && task.sub_tasks_count != null && task.sub_tasks_count !== 0 && ( -
- - {task.sub_tasks_count} - - -
+ +
+ + {task.sub_tasks_count} + + +
+
)} {/* Task indicators */}
{/* Comments count indicator - only show if count > 1 */} {task.comments_count != null && task.comments_count !== 0 && ( -
- -
+ +
+ +
+
)} {/* Subscribers indicator */} {task.has_subscribers && ( - + + + )} {/* Attachments count indicator - only show if count > 1 */} {task.attachments_count != null && task.attachments_count !== 0 && ( -
- -
+ +
+ +
+
)} {/* Dependencies indicator */} {task.has_dependencies && ( - + + + )} {/* Recurring task indicator */} {task.schedule_id && ( - + = ({ position, cla )}
- - {/* Active Filters Pills */} - {activeFiltersCount > 0 && ( -
- {searchValue && ( -
- - "{searchValue}" - -
- )} - - {filterSectionsData - .filter(section => section.id !== 'groupBy') // <-- skip groupBy - .flatMap(section => - section.selectedValues - .map(value => { - const option = section.options.find(opt => opt.value === value); - if (!option) return null; - - return ( -
- {option.color && ( -
- )} - {option.label} - -
- ); - }) - .filter(Boolean) - )} -
- )}
); }; diff --git a/worklenz-frontend/src/components/task-management/task-row.tsx b/worklenz-frontend/src/components/task-management/task-row.tsx index 884d364b..b482c0ee 100644 --- a/worklenz-frontend/src/components/task-management/task-row.tsx +++ b/worklenz-frontend/src/components/task-management/task-row.tsx @@ -1017,7 +1017,7 @@ const TaskRow: React.FC = React.memo(
{/* Comments indicator */} {(task as any).comments_count > 0 && ( - + @@ -1025,7 +1025,7 @@ const TaskRow: React.FC = React.memo( )} {/* Attachments indicator */} {(task as any).attachments_count > 0 && ( - + @@ -1033,7 +1033,7 @@ const TaskRow: React.FC = React.memo( )} {/* Dependencies indicator */} {(task as any).has_dependencies && ( - + @@ -1041,7 +1041,7 @@ const TaskRow: React.FC = React.memo( )} {/* Subscribers indicator */} {(task as any).has_subscribers && ( - + @@ -1049,7 +1049,7 @@ const TaskRow: React.FC = React.memo( )} {/* Recurring indicator */} {(task as any).schedule_id && ( - + diff --git a/worklenz-frontend/src/features/task-management/task-management.slice.ts b/worklenz-frontend/src/features/task-management/task-management.slice.ts index 4ec7d68b..7a7a0177 100644 --- a/worklenz-frontend/src/features/task-management/task-management.slice.ts +++ b/worklenz-frontend/src/features/task-management/task-management.slice.ts @@ -908,6 +908,38 @@ const taskManagementSlice = createSlice({ return column; }); }, + // Add action to update task counts (comments, attachments, etc.) + updateTaskCounts: (state, action: PayloadAction<{ + taskId: string; + counts: { + comments_count?: number; + attachments_count?: number; + has_subscribers?: boolean; + has_dependencies?: boolean; + schedule_id?: string | null; // Add schedule_id for recurring tasks + }; + }>) => { + const { taskId, counts } = action.payload; + const task = state.entities[taskId]; + if (task) { + // Update only the provided count fields + if (counts.comments_count !== undefined) { + task.comments_count = counts.comments_count; + } + if (counts.attachments_count !== undefined) { + task.attachments_count = counts.attachments_count; + } + if (counts.has_subscribers !== undefined) { + task.has_subscribers = counts.has_subscribers; + } + if (counts.has_dependencies !== undefined) { + task.has_dependencies = counts.has_dependencies; + } + if (counts.schedule_id !== undefined) { + task.schedule_id = counts.schedule_id; + } + } + }, }, extraReducers: builder => { builder @@ -1100,6 +1132,7 @@ export const { updateCustomColumn, deleteCustomColumn, syncColumnsWithFields, + updateTaskCounts, } = taskManagementSlice.actions; // Export the selectors diff --git a/worklenz-frontend/src/features/task-management/taskListFields.slice.ts b/worklenz-frontend/src/features/task-management/taskListFields.slice.ts index 2c0cd472..51a9d474 100644 --- a/worklenz-frontend/src/features/task-management/taskListFields.slice.ts +++ b/worklenz-frontend/src/features/task-management/taskListFields.slice.ts @@ -1,6 +1,7 @@ import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit'; import { updateColumnVisibility } from './task-management.slice'; import { ITaskListColumn } from '@/types/tasks/taskList.types'; +import logger from '@/utils/errorLogger'; export interface TaskListField { key: string; @@ -39,7 +40,7 @@ function loadFields(): TaskListField[] { const parsed = JSON.parse(stored); return parsed; } catch (error) { - console.warn('Failed to parse stored fields, using defaults:', error); + logger.error('Failed to parse stored fields, using defaults:', error); } } @@ -47,7 +48,6 @@ function loadFields(): TaskListField[] { } function saveFields(fields: TaskListField[]) { - console.log('Saving fields to localStorage:', fields); localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(fields)); } diff --git a/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts b/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts index 19b78dd4..c1696ae4 100644 --- a/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts +++ b/worklenz-frontend/src/hooks/useTaskSocketHandlers.ts @@ -592,13 +592,18 @@ export const useTaskSocketHandlers = () => { ); const handleTaskSubscribersChange = useCallback( - (data: InlineMember[]) => { - if (!data) return; - dispatch(setTaskSubscribers(data)); + (subscribers: InlineMember[]) => { + if (!subscribers) return; + dispatch(setTaskSubscribers(subscribers)); + + // Note: We don't have task_id in this event, so we can't update the task-management slice + // The has_subscribers field will be updated when the task is refetched }, [dispatch] ); + + const handleEstimationChange = useCallback( (task: { id: string; parent_task: string | null; estimation: number }) => { if (!task) return; @@ -848,6 +853,7 @@ export const useTaskSocketHandlers = () => { { event: SocketEvents.QUICK_TASK.toString(), handler: handleNewTaskReceived }, { event: SocketEvents.TASK_PROGRESS_UPDATED.toString(), handler: handleTaskProgressUpdated }, { event: SocketEvents.TASK_CUSTOM_COLUMN_UPDATE.toString(), handler: handleCustomColumnUpdate }, + ]; // Register all event listeners @@ -879,5 +885,6 @@ export const useTaskSocketHandlers = () => { handleNewTaskReceived, handleTaskProgressUpdated, handleCustomColumnUpdate, + ]); };