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.
This commit is contained in:
chamiakJ
2025-07-07 07:05:29 +05:30
parent 03fc2fb7ee
commit f9926e7a5d
22 changed files with 304 additions and 158 deletions

View File

@@ -108,5 +108,19 @@
"key": "Çelësi", "key": "Çelësi",
"formula": "Formula" "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"
}
} }
} }

View File

@@ -108,5 +108,19 @@
"key": "Schlüssel", "key": "Schlüssel",
"formula": "Formel" "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"
}
} }
} }

View File

@@ -108,5 +108,19 @@
"key": "Key", "key": "Key",
"formula": "Formula" "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"
}
} }
} }

View File

@@ -17,5 +17,19 @@
"renamePhase": "Rename Phase", "renamePhase": "Rename Phase",
"changeCategory": "Change Category", "changeCategory": "Change Category",
"clickToEditGroupName": "Click to edit group name", "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"
}
}
} }

View File

@@ -108,5 +108,19 @@
"key": "Clave", "key": "Clave",
"formula": "Fórmula" "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"
}
} }
} }

View File

@@ -108,5 +108,19 @@
"key": "Chave", "key": "Chave",
"formula": "Fórmula" "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"
}
} }
} }

View File

@@ -193,28 +193,30 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
{/* Subtask Section - only show if count > 1 */} {/* Subtask Section - only show if count > 1 */}
{task.sub_tasks_count != null && Number(task.sub_tasks_count) > 1 && ( {task.sub_tasks_count != null && Number(task.sub_tasks_count) > 1 && (
<Button <Tooltip title={t(`indicators.tooltips.subtasks${Number(task.sub_tasks_count) === 1 ? '' : '_plural'}`, { count: Number(task.sub_tasks_count) })}>
onClick={handleSubtaskButtonClick} <Button
size="small" onClick={handleSubtaskButtonClick}
style={{ size="small"
padding: 0,
}}
type="text"
>
<Tag
bordered={false}
style={{ style={{
display: 'flex', padding: 0,
alignItems: 'center',
margin: 0,
backgroundColor: themeWiseColor('white', '#1e1e1e', themeMode),
}} }}
type="text"
> >
<ForkOutlined rotate={90} /> <Tag
<span>{task.sub_tasks_count || 0}</span> bordered={false}
{task.show_sub_tasks ? <CaretDownFilled /> : <CaretRightFilled />} style={{
</Tag> display: 'flex',
</Button> alignItems: 'center',
margin: 0,
backgroundColor: themeWiseColor('white', '#1e1e1e', themeMode),
}}
>
<ForkOutlined rotate={90} />
<span>{task.sub_tasks_count || 0}</span>
{task.show_sub_tasks ? <CaretDownFilled /> : <CaretRightFilled />}
</Tag>
</Button>
</Tooltip>
)} )}
</Flex> </Flex>
</Flex> </Flex>

View File

@@ -10,6 +10,7 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types'; import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { IGroupBy } from '@/features/tasks/tasks.slice'; import { IGroupBy } from '@/features/tasks/tasks.slice';
import { useTranslation } from 'react-i18next';
const { Text } = Typography; const { Text } = Typography;
@@ -36,6 +37,7 @@ const KanbanTaskCard: React.FC<TaskRowProps> = ({
onSelect, onSelect,
onToggleSubtasks, onToggleSubtasks,
}) => { }) => {
const { t } = useTranslation('task-list-table');
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: task.id!, id: task.id!,
data: { data: {
@@ -196,14 +198,18 @@ const KanbanTaskCard: React.FC<TaskRowProps> = ({
</span> </span>
)} )}
{task.comments_count && task.comments_count > 1 && ( {task.comments_count && task.comments_count > 1 && (
<span className="kanban-task-indicator"> <Tooltip title={t(`indicators.tooltips.comments${task.comments_count === 1 ? '' : '_plural'}`, { count: task.comments_count })}>
<MessageOutlined /> {task.comments_count} <span className="kanban-task-indicator">
</span> <MessageOutlined /> {task.comments_count}
</span>
</Tooltip>
)} )}
{task.attachments_count && task.attachments_count > 1 && ( {task.attachments_count && task.attachments_count > 1 && (
<span className="kanban-task-indicator"> <Tooltip title={t(`indicators.tooltips.attachments${task.attachments_count === 1 ? '' : '_plural'}`, { count: task.attachments_count })}>
<PaperClipOutlined /> {task.attachments_count} <span className="kanban-task-indicator">
</span> <PaperClipOutlined /> {task.attachments_count}
</span>
</Tooltip>
)} )}
</div> </div>
</Space> </Space>

View File

@@ -15,6 +15,8 @@ import taskAttachmentsApiService from '@/api/tasks/task-attachments.api.service'
import logger from '@/utils/errorLogger'; import logger from '@/utils/errorLogger';
import taskCommentsApiService from '@/api/tasks/task-comments.api.service'; import taskCommentsApiService from '@/api/tasks/task-comments.api.service';
import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { updateTaskCounts } from '@/features/task-management/task-management.slice';
interface AttachmentsPreviewProps { interface AttachmentsPreviewProps {
attachment: ITaskAttachmentViewModel; attachment: ITaskAttachmentViewModel;
@@ -28,6 +30,7 @@ const AttachmentsPreview = ({
isCommentAttachment = false, isCommentAttachment = false,
}: AttachmentsPreviewProps) => { }: AttachmentsPreviewProps) => {
const { selectedTaskId } = useAppSelector(state => state.taskDrawerReducer); const { selectedTaskId } = useAppSelector(state => state.taskDrawerReducer);
const dispatch = useAppDispatch();
const [deleting, setDeleting] = useState(false); const [deleting, setDeleting] = useState(false);
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const [currentFileUrl, setCurrentFileUrl] = useState<string | null>(null); const [currentFileUrl, setCurrentFileUrl] = useState<string | null>(null);
@@ -88,6 +91,7 @@ const AttachmentsPreview = ({
if (isCommentAttachment) { if (isCommentAttachment) {
const res = await taskCommentsApiService.deleteAttachment(id, selectedTaskId); const res = await taskCommentsApiService.deleteAttachment(id, selectedTaskId);
if (res.done) { if (res.done) {
// Let the parent component handle the refetch and Redux update
document.dispatchEvent(new Event('task-comment-update')); document.dispatchEvent(new Event('task-comment-update'));
} }
} else { } else {
@@ -96,6 +100,7 @@ const AttachmentsPreview = ({
if (onDelete) { if (onDelete) {
onDelete(id); onDelete(id);
} }
// Parent component will handle the refetch and Redux update
} }
} }
} catch (e) { } catch (e) {

View File

@@ -13,12 +13,16 @@ import logger from '@/utils/errorLogger';
import TaskViewCommentEdit from './task-view-comment-edit'; import TaskViewCommentEdit from './task-view-comment-edit';
import './task-comments.css'; import './task-comments.css';
import { useAppSelector } from '@/hooks/useAppSelector'; 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 { themeWiseColor } from '@/utils/themeWiseColor';
import { colors } from '@/styles/colors'; import { colors } from '@/styles/colors';
import AttachmentsGrid from '../attachments/attachments-grid'; import AttachmentsGrid from '../attachments/attachments-grid';
import { TFunction } from 'i18next'; import { TFunction } from 'i18next';
import SingleAvatar from '@/components/common/single-avatar/single-avatar'; import SingleAvatar from '@/components/common/single-avatar/single-avatar';
import { sanitizeHtml } from '@/utils/sanitizeInput'; import { sanitizeHtml } from '@/utils/sanitizeInput';
import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
// Helper function to format date for time separators // Helper function to format date for time separators
const formatDateForSeparator = (date: string) => { const formatDateForSeparator = (date: string) => {
@@ -90,6 +94,8 @@ const TaskComments = ({ taskId, t }: { taskId?: string; t: TFunction }) => {
const auth = useAuthService(); const auth = useAuthService();
const themeMode = useAppSelector(state => state.themeReducer.mode); const themeMode = useAppSelector(state => state.themeReducer.mode);
const currentUserId = auth.getCurrentSession()?.id; const currentUserId = auth.getCurrentSession()?.id;
const { socket, connected } = useSocket();
const dispatch = useAppDispatch();
const getComments = useCallback( const getComments = useCallback(
async (showLoading = true) => { async (showLoading = true) => {
@@ -115,6 +121,14 @@ const TaskComments = ({ taskId, t }: { taskId?: string; t: TFunction }) => {
}); });
setComments(sortedComments); setComments(sortedComments);
// Update Redux state with the current comment count
dispatch(updateTaskCounts({
taskId,
counts: {
comments_count: sortedComments.length
}
}));
} }
setLoading(false); setLoading(false);
@@ -123,7 +137,7 @@ const TaskComments = ({ taskId, t }: { taskId?: string; t: TFunction }) => {
setLoading(false); setLoading(false);
} }
}, },
[taskId] [taskId, dispatch]
); );
useEffect(() => { useEffect(() => {
@@ -196,8 +210,11 @@ const TaskComments = ({ taskId, t }: { taskId?: string; t: TFunction }) => {
try { try {
const res = await taskCommentsApiService.delete(id, taskId); const res = await taskCommentsApiService.delete(id, taskId);
if (res.done) { if (res.done) {
// Refresh comments to get updated list
await getComments(false); 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) { } catch (e) {
logger.error('Error deleting comment', e); logger.error('Error deleting comment', e);
@@ -227,7 +244,9 @@ const TaskComments = ({ taskId, t }: { taskId?: string; t: TFunction }) => {
await getComments(false); await getComments(false);
// Dispatch event to notify that an attachment was deleted // 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) { } catch (e) {
logger.error('Error deleting attachment', e); logger.error('Error deleting attachment', e);

View File

@@ -6,6 +6,8 @@ import logger from '@/utils/errorLogger';
import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppSelector } from '@/hooks/useAppSelector';
import { themeWiseColor } from '@/utils/themeWiseColor'; import { themeWiseColor } from '@/utils/themeWiseColor';
import { colors } from '@/styles/colors'; import { colors } from '@/styles/colors';
import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
interface TaskViewCommentEditProps { interface TaskViewCommentEditProps {
commentData: ITaskCommentViewModel; commentData: ITaskCommentViewModel;
@@ -27,6 +29,7 @@ const TaskViewCommentEdit = ({ commentData, onUpdated }: TaskViewCommentEditProp
const themeMode = useAppSelector(state => state.themeReducer.mode); const themeMode = useAppSelector(state => state.themeReducer.mode);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [content, setContent] = useState(''); const [content, setContent] = useState('');
const { socket, connected } = useSocket();
// Initialize content when component mounts // Initialize content when component mounts
useEffect(() => { useEffect(() => {
@@ -55,7 +58,9 @@ const TaskViewCommentEdit = ({ commentData, onUpdated }: TaskViewCommentEditProp
onUpdated(commentData); onUpdated(commentData);
// Dispatch event to notify that a comment was updated // 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) { } catch (e) {
logger.error('Error updating comment', e); logger.error('Error updating comment', e);

View File

@@ -14,6 +14,8 @@ import {
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { DeleteOutlined, ExclamationCircleFilled } from '@ant-design/icons'; import { DeleteOutlined, ExclamationCircleFilled } from '@ant-design/icons';
import { useAppSelector } from '@/hooks/useAppSelector'; 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 { colors } from '@/styles/colors';
import { TFunction } from 'i18next'; import { TFunction } from 'i18next';
import { IDependencyType, ITaskDependency } from '@/types/tasks/task-dependency.types'; import { IDependencyType, ITaskDependency } from '@/types/tasks/task-dependency.types';
@@ -40,6 +42,7 @@ const DependenciesTable = ({
const [hoverRow, setHoverRow] = useState<string | null>(null); const [hoverRow, setHoverRow] = useState<string | null>(null);
const [isDependencyInputShow, setIsDependencyInputShow] = useState(false); const [isDependencyInputShow, setIsDependencyInputShow] = useState(false);
const { projectId } = useAppSelector(state => state.projectReducer); const { projectId } = useAppSelector(state => state.projectReducer);
const dispatch = useAppDispatch();
const [taskList, setTaskList] = useState<{ label: string; value: string }[]>([]); const [taskList, setTaskList] = useState<{ label: string; value: string }[]>([]);
const [loadingTaskList, setLoadingTaskList] = useState(false); const [loadingTaskList, setLoadingTaskList] = useState(false);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
@@ -60,6 +63,14 @@ const DependenciesTable = ({
setIsDependencyInputShow(false); setIsDependencyInputShow(false);
setTaskList([]); setTaskList([]);
setSearchTerm(''); setSearchTerm('');
// Update Redux state with dependency status
dispatch(updateTaskCounts({
taskId: task.id,
counts: {
has_dependencies: true
}
}));
} }
} catch (error) { } catch (error) {
console.error('Error adding dependency:', error); console.error('Error adding dependency:', error);
@@ -89,6 +100,16 @@ const DependenciesTable = ({
const res = await taskDependenciesApiService.deleteTaskDependency(dependencyId); const res = await taskDependenciesApiService.deleteTaskDependency(dependencyId);
if (res.done) { if (res.done) {
refreshTaskDependencies(); 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) { } catch (error) {
console.error('Error deleting dependency:', error); console.error('Error deleting dependency:', error);

View File

@@ -25,6 +25,7 @@ import { ITaskViewModel } from '@/types/tasks/task.types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppDispatch } from '@/hooks/useAppDispatch';
import { updateRecurringChange } from '@/features/tasks/tasks.slice'; 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 { taskRecurringApiService } from '@/api/tasks/task-recurring.api.service';
import logger from '@/utils/errorLogger'; import logger from '@/utils/errorLogger';
import { setTaskRecurringSchedule } from '@/features/task-drawer/task-drawer.slice'; 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 }) 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); setRecurring(checked);
if (!checked) setShowConfig(false); if (!checked) setShowConfig(false);
} }

View File

@@ -2,6 +2,8 @@ import { Button, Flex, Form, Mentions, Space, Tooltip, Typography, message } fro
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { PaperClipOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons'; import { PaperClipOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons';
import { useAppSelector } from '@/hooks/useAppSelector'; 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 { colors } from '@/styles/colors';
import { themeWiseColor } from '@/utils/themeWiseColor'; import { themeWiseColor } from '@/utils/themeWiseColor';
import { formatDateTimeWithLocale } from '@/utils/format-date-time-with-locale'; import { formatDateTimeWithLocale } from '@/utils/format-date-time-with-locale';
@@ -47,6 +49,7 @@ const InfoTabFooter = () => {
const { taskFormViewModel, selectedTaskId } = useAppSelector(state => state.taskDrawerReducer); const { taskFormViewModel, selectedTaskId } = useAppSelector(state => state.taskDrawerReducer);
const { projectId } = useAppSelector(state => state.projectReducer); const { projectId } = useAppSelector(state => state.projectReducer);
const dispatch = useAppDispatch();
const [members, setMembers] = useState<ITeamMember[]>([]); const [members, setMembers] = useState<ITeamMember[]>([]);
const [membersLoading, setMembersLoading] = useState<boolean>(false); const [membersLoading, setMembersLoading] = useState<boolean>(false);
@@ -156,8 +159,10 @@ const InfoTabFooter = () => {
setCommentValue(''); setCommentValue('');
// Dispatch event to notify that a comment was created // Dispatch event to notify that a comment was created
// This will trigger scrolling to the new comment // This will trigger the task comments component to refresh and update Redux
document.dispatchEvent(new Event('task-comment-create')); document.dispatchEvent(new CustomEvent('task-comment-create', {
detail: { taskId: selectedTaskId }
}));
} }
} catch (error) { } catch (error) {
logger.error('Failed to create comment:', error); logger.error('Failed to create comment:', error);

View File

@@ -15,6 +15,8 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { TFunction } from 'i18next'; import { TFunction } from 'i18next';
import { useAppSelector } from '@/hooks/useAppSelector'; 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 { ITaskViewModel } from '@/types/tasks/task.types';
import { ITeamMembersViewModel } from '@/types/teamMembers/teamMembersViewModel.types'; import { ITeamMembersViewModel } from '@/types/teamMembers/teamMembersViewModel.types';
import { teamMembersApiService } from '@/api/team-members/teamMembers.api.service'; 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 Avatars from '@/components/avatars/avatars';
import { tasksApiService } from '@/api/tasks/tasks.api.service'; import { tasksApiService } from '@/api/tasks/tasks.api.service';
import { setTaskSubscribers } from '@/features/task-drawer/task-drawer.slice'; import { setTaskSubscribers } from '@/features/task-drawer/task-drawer.slice';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types'; import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types';
import useTabSearchParam from '@/hooks/useTabSearchParam'; import useTabSearchParam from '@/hooks/useTabSearchParam';
import { InlineMember } from '@/types/teamMembers/inlineMember.types'; 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?.emit(SocketEvents.TASK_SUBSCRIBERS_CHANGE.toString(), body);
socket?.once(SocketEvents.TASK_SUBSCRIBERS_CHANGE.toString(), (data: InlineMember[]) => { socket?.once(SocketEvents.TASK_SUBSCRIBERS_CHANGE.toString(), (data: InlineMember[]) => {
dispatch(setTaskSubscribers(data)); dispatch(setTaskSubscribers(data));
// Update Redux state with subscriber status
dispatch(updateTaskCounts({
taskId: task.id,
counts: {
has_subscribers: data && data.length > 0
}
}));
}); });
} catch (error) { } catch (error) {
logger.error('Error notifying member:', error); logger.error('Error notifying member:', error);

View File

@@ -8,6 +8,7 @@ import { useAppSelector } from '@/hooks/useAppSelector';
import TaskDetailsForm from './task-details-form'; import TaskDetailsForm from './task-details-form';
import { fetchTask } from '@/features/tasks/tasks.slice'; import { fetchTask } from '@/features/tasks/tasks.slice';
import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppDispatch } from '@/hooks/useAppDispatch';
import { updateTaskCounts } from '@/features/task-management/task-management.slice';
import { TFunction } from 'i18next'; import { TFunction } from 'i18next';
import { subTasksApiService } from '@/api/tasks/subtasks.api.service'; import { subTasksApiService } from '@/api/tasks/subtasks.api.service';
import { ISubTask } from '@/types/tasks/subTask.types'; import { ISubTask } from '@/types/tasks/subTask.types';
@@ -24,6 +25,7 @@ import AttachmentsGrid from './attachments/attachments-grid';
import TaskComments from './comments/task-comments'; import TaskComments from './comments/task-comments';
import { ITaskCommentViewModel } from '@/types/tasks/task-comments.types'; import { ITaskCommentViewModel } from '@/types/tasks/task-comments.types';
import taskCommentsApiService from '@/api/tasks/task-comments.api.service'; import taskCommentsApiService from '@/api/tasks/task-comments.api.service';
import { ITaskViewModel } from '@/types/tasks/task.types';
interface TaskDrawerInfoTabProps { interface TaskDrawerInfoTabProps {
t: TFunction; t: TFunction;
@@ -148,7 +150,7 @@ const TaskDrawerInfoTab = ({ t }: TaskDrawerInfoTabProps) => {
label: <Typography.Text strong>{t('taskInfoTab.dependencies.title')}</Typography.Text>, label: <Typography.Text strong>{t('taskInfoTab.dependencies.title')}</Typography.Text>,
children: ( children: (
<DependenciesTable <DependenciesTable
task={taskFormViewModel?.task || {}} task={(taskFormViewModel?.task as ITaskViewModel) || {} as ITaskViewModel}
t={t} t={t}
taskDependencies={taskDependencies} taskDependencies={taskDependencies}
loadingTaskDependencies={loadingTaskDependencies} loadingTaskDependencies={loadingTaskDependencies}
@@ -214,6 +216,14 @@ const TaskDrawerInfoTab = ({ t }: TaskDrawerInfoTabProps) => {
const res = await taskDependenciesApiService.getTaskDependencies(selectedTaskId); const res = await taskDependenciesApiService.getTaskDependencies(selectedTaskId);
if (res.done) { if (res.done) {
setTaskDependencies(res.body); setTaskDependencies(res.body);
// Update Redux state with the current dependency status
dispatch(updateTaskCounts({
taskId: selectedTaskId,
counts: {
has_dependencies: res.body.length > 0
}
}));
} }
} catch (error) { } catch (error) {
logger.error('Error fetching task dependencies:', error); logger.error('Error fetching task dependencies:', error);
@@ -229,6 +239,14 @@ const TaskDrawerInfoTab = ({ t }: TaskDrawerInfoTabProps) => {
const res = await taskAttachmentsApiService.getTaskAttachments(selectedTaskId); const res = await taskAttachmentsApiService.getTaskAttachments(selectedTaskId);
if (res.done) { if (res.done) {
setTaskAttachments(res.body); setTaskAttachments(res.body);
// Update Redux state with the current attachment count
dispatch(updateTaskCounts({
taskId: selectedTaskId,
counts: {
attachments_count: res.body.length
}
}));
} }
} catch (error) { } catch (error) {
logger.error('Error fetching task attachments:', error); logger.error('Error fetching task attachments:', error);

View File

@@ -328,55 +328,65 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumn
{/* Subtask count indicator - only show if count > 1 */} {/* Subtask count indicator - only show if count > 1 */}
{!isSubtask && task.sub_tasks_count != null && task.sub_tasks_count !== 0 && ( {!isSubtask && task.sub_tasks_count != null && task.sub_tasks_count !== 0 && (
<div className="flex items-center gap-1 px-2 py-1 bg-blue-50 dark:bg-blue-900/20 rounded-md"> <Tooltip title={t(`indicators.tooltips.subtasks${task.sub_tasks_count === 1 ? '' : '_plural'}`, { count: task.sub_tasks_count })}>
<span className="text-xs text-blue-600 dark:text-blue-400 font-medium"> <div className="flex items-center gap-1 px-2 py-1 bg-blue-50 dark:bg-blue-900/20 rounded-md">
{task.sub_tasks_count} <span className="text-xs text-blue-600 dark:text-blue-400 font-medium">
</span> {task.sub_tasks_count}
<DoubleRightOutlined className="text-xs text-blue-600 dark:text-blue-400" /> </span>
</div> <DoubleRightOutlined className="text-xs text-blue-600 dark:text-blue-400" />
</div>
</Tooltip>
)} )}
{/* Task indicators */} {/* Task indicators */}
<div className="flex items-center gap-1 ml-2"> <div className="flex items-center gap-1 ml-2">
{/* Comments count indicator - only show if count > 1 */} {/* Comments count indicator - only show if count > 1 */}
{task.comments_count != null && task.comments_count !== 0 && ( {task.comments_count != null && task.comments_count !== 0 && (
<div className="flex items-center gap-1"> <Tooltip title={t(`indicators.tooltips.comments${task.comments_count === 1 ? '' : '_plural'}`, { count: task.comments_count })}>
<CommentOutlined <div className="flex items-center gap-1">
className="text-gray-500 dark:text-gray-400" <CommentOutlined
style={{ fontSize: 14 }} className="text-gray-500 dark:text-gray-400"
/> style={{ fontSize: 14 }}
</div> />
</div>
</Tooltip>
)} )}
{/* Subscribers indicator */} {/* Subscribers indicator */}
{task.has_subscribers && ( {task.has_subscribers && (
<EyeOutlined <Tooltip title={t('indicators.tooltips.subscribers')}>
className="text-gray-500 dark:text-gray-400" <EyeOutlined
style={{ fontSize: 14 }} className="text-gray-500 dark:text-gray-400"
/> style={{ fontSize: 14 }}
/>
</Tooltip>
)} )}
{/* Attachments count indicator - only show if count > 1 */} {/* Attachments count indicator - only show if count > 1 */}
{task.attachments_count != null && task.attachments_count !== 0 && ( {task.attachments_count != null && task.attachments_count !== 0 && (
<div className="flex items-center gap-1"> <Tooltip title={t(`indicators.tooltips.attachments${task.attachments_count === 1 ? '' : '_plural'}`, { count: task.attachments_count })}>
<PaperClipOutlined <div className="flex items-center gap-1">
className="text-gray-500 dark:text-gray-400" <PaperClipOutlined
style={{ fontSize: 14 }} className="text-gray-500 dark:text-gray-400"
/> style={{ fontSize: 14 }}
</div> />
</div>
</Tooltip>
)} )}
{/* Dependencies indicator */} {/* Dependencies indicator */}
{task.has_dependencies && ( {task.has_dependencies && (
<MinusCircleOutlined <Tooltip title={t('indicators.tooltips.dependencies')}>
className="text-gray-500 dark:text-gray-400" <MinusCircleOutlined
style={{ fontSize: 14 }} className="text-gray-500 dark:text-gray-400"
/> style={{ fontSize: 14 }}
/>
</Tooltip>
)} )}
{/* Recurring task indicator */} {/* Recurring task indicator */}
{task.schedule_id && ( {task.schedule_id && (
<Tooltip title="Recurring Task"> <Tooltip title={t('indicators.tooltips.recurring')}>
<RetweetOutlined <RetweetOutlined
className="text-gray-500 dark:text-gray-400" className="text-gray-500 dark:text-gray-400"
style={{ fontSize: 14 }} style={{ fontSize: 14 }}

View File

@@ -1297,93 +1297,6 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({ position, cla
)} )}
</div> </div>
</div> </div>
{/* Active Filters Pills */}
{activeFiltersCount > 0 && (
<div
className={`flex flex-wrap items-center gap-1.5 mt-2 pt-2 border-t ${themeClasses.dividerBorder}`}
>
{searchValue && (
<div
className={`inline-flex items-center gap-1 px-1.5 py-0.5 text-xs font-medium rounded-full ${themeClasses.pillActiveBg} ${themeClasses.pillActiveText}`}
>
<SearchOutlined className="w-2.5 h-2.5" />
<span>"{searchValue}"</span>
<button
onClick={() => {
if (projectId) {
// Cancel pending search and immediately clear
debouncedSearchChangeRef.current?.cancel();
if (position === 'board') {
dispatch(setKanbanSearch(''));
dispatch(fetchEnhancedKanbanGroups(projectId));
} else {
// Always use taskReducer search for list view
dispatch(setSearch(''));
dispatch(fetchTasksV3(projectId));
}
}
}}
className={`ml-1 rounded-full p-0.5 transition-colors duration-150 ${
isDarkMode ? 'hover:bg-gray-800' : 'hover:bg-gray-200'
}`}
>
<CloseOutlined className="w-2.5 h-2.5" />
</button>
</div>
)}
{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 (
<div
key={`${section.id}-${value}`}
className={`inline-flex items-center gap-1 px-1.5 py-0.5 text-xs font-medium rounded-full ${themeClasses.pillBg} ${themeClasses.pillText}`}
>
{option.color && (
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: option.color }}
/>
)}
<span>{option.label}</span>
<button
onClick={() => {
// Update local state immediately for UI feedback
setFilterSections(prev =>
prev.map(s =>
s.id === section.id
? {
...s,
selectedValues: s.selectedValues.filter(v => v !== value),
}
: s
)
);
// Use debounced API call
const newValues = section.selectedValues.filter(v => v !== value);
handleSelectionChange(section.id, newValues);
}}
className={`ml-1 rounded-full p-0.5 transition-colors duration-150 ${
isDarkMode ? 'hover:bg-gray-600' : 'hover:bg-gray-200'
}`}
>
<CloseOutlined className="w-2.5 h-2.5" />
</button>
</div>
);
})
.filter(Boolean)
)}
</div>
)}
</div> </div>
); );
}; };

View File

@@ -1017,7 +1017,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(
<div className="task-indicators flex items-center gap-2"> <div className="task-indicators flex items-center gap-2">
{/* Comments indicator */} {/* Comments indicator */}
{(task as any).comments_count > 0 && ( {(task as any).comments_count > 0 && (
<Tooltip title={t('taskManagement.comments', 'Comments')}> <Tooltip title={t(`task-management:indicators.tooltips.comments${(task as any).comments_count === 1 ? '' : '_plural'}`, { count: (task as any).comments_count })}>
<MessageOutlined <MessageOutlined
style={{ fontSize: 14, color: isDarkMode ? '#b0b3b8' : '#888' }} style={{ fontSize: 14, color: isDarkMode ? '#b0b3b8' : '#888' }}
/> />
@@ -1025,7 +1025,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(
)} )}
{/* Attachments indicator */} {/* Attachments indicator */}
{(task as any).attachments_count > 0 && ( {(task as any).attachments_count > 0 && (
<Tooltip title={t('taskManagement.attachments', 'Attachments')}> <Tooltip title={t(`task-management:indicators.tooltips.attachments${(task as any).attachments_count === 1 ? '' : '_plural'}`, { count: (task as any).attachments_count })}>
<PaperClipOutlined <PaperClipOutlined
style={{ fontSize: 14, color: isDarkMode ? '#b0b3b8' : '#888' }} style={{ fontSize: 14, color: isDarkMode ? '#b0b3b8' : '#888' }}
/> />
@@ -1033,7 +1033,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(
)} )}
{/* Dependencies indicator */} {/* Dependencies indicator */}
{(task as any).has_dependencies && ( {(task as any).has_dependencies && (
<Tooltip title={t('taskManagement.dependencies', 'Dependencies')}> <Tooltip title={t('task-management:indicators.tooltips.dependencies')}>
<MinusCircleOutlined <MinusCircleOutlined
style={{ fontSize: 14, color: isDarkMode ? '#b0b3b8' : '#888' }} style={{ fontSize: 14, color: isDarkMode ? '#b0b3b8' : '#888' }}
/> />
@@ -1041,7 +1041,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(
)} )}
{/* Subscribers indicator */} {/* Subscribers indicator */}
{(task as any).has_subscribers && ( {(task as any).has_subscribers && (
<Tooltip title={t('taskManagement.subscribers', 'Subscribers')}> <Tooltip title={t('task-management:indicators.tooltips.subscribers')}>
<EyeOutlined <EyeOutlined
style={{ fontSize: 14, color: isDarkMode ? '#b0b3b8' : '#888' }} style={{ fontSize: 14, color: isDarkMode ? '#b0b3b8' : '#888' }}
/> />
@@ -1049,7 +1049,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(
)} )}
{/* Recurring indicator */} {/* Recurring indicator */}
{(task as any).schedule_id && ( {(task as any).schedule_id && (
<Tooltip title={t('taskManagement.recurringTask', 'Recurring Task')}> <Tooltip title={t('task-management:indicators.tooltips.recurring')}>
<RetweetOutlined <RetweetOutlined
style={{ fontSize: 14, color: isDarkMode ? '#b0b3b8' : '#888' }} style={{ fontSize: 14, color: isDarkMode ? '#b0b3b8' : '#888' }}
/> />

View File

@@ -908,6 +908,38 @@ const taskManagementSlice = createSlice({
return column; 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 => { extraReducers: builder => {
builder builder
@@ -1100,6 +1132,7 @@ export const {
updateCustomColumn, updateCustomColumn,
deleteCustomColumn, deleteCustomColumn,
syncColumnsWithFields, syncColumnsWithFields,
updateTaskCounts,
} = taskManagementSlice.actions; } = taskManagementSlice.actions;
// Export the selectors // Export the selectors

View File

@@ -1,6 +1,7 @@
import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit'; import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
import { updateColumnVisibility } from './task-management.slice'; import { updateColumnVisibility } from './task-management.slice';
import { ITaskListColumn } from '@/types/tasks/taskList.types'; import { ITaskListColumn } from '@/types/tasks/taskList.types';
import logger from '@/utils/errorLogger';
export interface TaskListField { export interface TaskListField {
key: string; key: string;
@@ -39,7 +40,7 @@ function loadFields(): TaskListField[] {
const parsed = JSON.parse(stored); const parsed = JSON.parse(stored);
return parsed; return parsed;
} catch (error) { } 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[]) { function saveFields(fields: TaskListField[]) {
console.log('Saving fields to localStorage:', fields);
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(fields)); localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(fields));
} }

View File

@@ -592,13 +592,18 @@ export const useTaskSocketHandlers = () => {
); );
const handleTaskSubscribersChange = useCallback( const handleTaskSubscribersChange = useCallback(
(data: InlineMember[]) => { (subscribers: InlineMember[]) => {
if (!data) return; if (!subscribers) return;
dispatch(setTaskSubscribers(data)); 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] [dispatch]
); );
const handleEstimationChange = useCallback( const handleEstimationChange = useCallback(
(task: { id: string; parent_task: string | null; estimation: number }) => { (task: { id: string; parent_task: string | null; estimation: number }) => {
if (!task) return; if (!task) return;
@@ -848,6 +853,7 @@ export const useTaskSocketHandlers = () => {
{ event: SocketEvents.QUICK_TASK.toString(), handler: handleNewTaskReceived }, { event: SocketEvents.QUICK_TASK.toString(), handler: handleNewTaskReceived },
{ event: SocketEvents.TASK_PROGRESS_UPDATED.toString(), handler: handleTaskProgressUpdated }, { event: SocketEvents.TASK_PROGRESS_UPDATED.toString(), handler: handleTaskProgressUpdated },
{ event: SocketEvents.TASK_CUSTOM_COLUMN_UPDATE.toString(), handler: handleCustomColumnUpdate }, { event: SocketEvents.TASK_CUSTOM_COLUMN_UPDATE.toString(), handler: handleCustomColumnUpdate },
]; ];
// Register all event listeners // Register all event listeners
@@ -879,5 +885,6 @@ export const useTaskSocketHandlers = () => {
handleNewTaskReceived, handleNewTaskReceived,
handleTaskProgressUpdated, handleTaskProgressUpdated,
handleCustomColumnUpdate, handleCustomColumnUpdate,
]); ]);
}; };