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:
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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' }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user