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:
@@ -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<string | null>(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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<string | null>(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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<ITeamMember[]>([]);
|
||||
const [membersLoading, setMembersLoading] = useState<boolean>(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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: <Typography.Text strong>{t('taskInfoTab.dependencies.title')}</Typography.Text>,
|
||||
children: (
|
||||
<DependenciesTable
|
||||
task={taskFormViewModel?.task || {}}
|
||||
task={(taskFormViewModel?.task as ITaskViewModel) || {} as ITaskViewModel}
|
||||
t={t}
|
||||
taskDependencies={taskDependencies}
|
||||
loadingTaskDependencies={loadingTaskDependencies}
|
||||
@@ -214,6 +216,14 @@ const TaskDrawerInfoTab = ({ t }: TaskDrawerInfoTabProps) => {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user