Merge branch 'release/v2.0.4' of https://github.com/Worklenz/worklenz into fix/bulk-action-bar
This commit is contained in:
3531
worklenz-backend/package-lock.json
generated
3531
worklenz-backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -86,7 +86,6 @@
|
|||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"pg": "^8.14.1",
|
"pg": "^8.14.1",
|
||||||
"pg-native": "^3.3.0",
|
|
||||||
"pug": "^3.0.2",
|
"pug": "^3.0.2",
|
||||||
"redis": "^4.6.7",
|
"redis": "^4.6.7",
|
||||||
"sanitize-html": "^2.11.0",
|
"sanitize-html": "^2.11.0",
|
||||||
|
|||||||
@@ -88,6 +88,23 @@ export async function on_task_status_change(_io: Server, socket: Socket, data?:
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Task is moving from "done" to "todo" or "doing" - reset manual_progress to FALSE
|
||||||
|
// so progress can be recalculated based on subtasks
|
||||||
|
await db.query(`
|
||||||
|
UPDATE tasks
|
||||||
|
SET manual_progress = FALSE
|
||||||
|
WHERE id = $1
|
||||||
|
`, [body.task_id]);
|
||||||
|
|
||||||
|
log(`Task ${body.task_id} moved from done status - manual_progress reset to FALSE`, null);
|
||||||
|
|
||||||
|
// If this is a subtask, update parent task progress
|
||||||
|
if (body.parent_task) {
|
||||||
|
setTimeout(() => {
|
||||||
|
socket.emit(SocketEvents.GET_TASK_PROGRESS.toString(), body.parent_task);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = await TasksControllerV2.getTaskCompleteRatio(body.parent_task || body.task_id);
|
const info = await TasksControllerV2.getTaskCompleteRatio(body.parent_task || body.task_id);
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import EnhancedKanbanCreateSection from './EnhancedKanbanCreateSection';
|
|||||||
import ImprovedTaskFilters from '../task-management/improved-task-filters';
|
import ImprovedTaskFilters from '../task-management/improved-task-filters';
|
||||||
import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
|
import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
|
||||||
import { useFilterDataLoader } from '@/hooks/useFilterDataLoader';
|
import { useFilterDataLoader } from '@/hooks/useFilterDataLoader';
|
||||||
|
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
|
||||||
|
|
||||||
// Import the TaskListFilters component
|
// Import the TaskListFilters component
|
||||||
const TaskListFilters = React.lazy(() => import('@/pages/projects/projectView/taskList/task-list-filters/task-list-filters'));
|
const TaskListFilters = React.lazy(() => import('@/pages/projects/projectView/taskList/task-list-filters/task-list-filters'));
|
||||||
@@ -71,10 +72,14 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
|
|||||||
const groupBy = useSelector((state: RootState) => state.enhancedKanbanReducer.groupBy);
|
const groupBy = useSelector((state: RootState) => state.enhancedKanbanReducer.groupBy);
|
||||||
const project = useAppSelector((state: RootState) => state.projectReducer.project);
|
const project = useAppSelector((state: RootState) => state.projectReducer.project);
|
||||||
const { statusCategories, status: existingStatuses } = useAppSelector((state) => state.taskStatusReducer);
|
const { statusCategories, status: existingStatuses } = useAppSelector((state) => state.taskStatusReducer);
|
||||||
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
|
|
||||||
// Load filter data
|
// Load filter data
|
||||||
useFilterDataLoader();
|
useFilterDataLoader();
|
||||||
|
|
||||||
|
// Set up socket event handlers for real-time updates
|
||||||
|
useTaskSocketHandlers();
|
||||||
|
|
||||||
// Local state for drag overlay
|
// Local state for drag overlay
|
||||||
const [activeTask, setActiveTask] = useState<any>(null);
|
const [activeTask, setActiveTask] = useState<any>(null);
|
||||||
const [activeGroup, setActiveGroup] = useState<any>(null);
|
const [activeGroup, setActiveGroup] = useState<any>(null);
|
||||||
@@ -441,18 +446,42 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
|
|||||||
|
|
||||||
<DragOverlay>
|
<DragOverlay>
|
||||||
{activeTask && (
|
{activeTask && (
|
||||||
<EnhancedKanbanTaskCard
|
<div
|
||||||
task={activeTask}
|
style={{
|
||||||
sectionId={activeTask.status_id || ''}
|
background: themeMode === 'dark' ? '#23272f' : '#fff',
|
||||||
isDragOverlay={true}
|
borderRadius: 8,
|
||||||
/>
|
boxShadow: '0 4px 16px rgba(0,0,0,0.12)',
|
||||||
|
padding: '12px 20px',
|
||||||
|
minWidth: 180,
|
||||||
|
maxWidth: 340,
|
||||||
|
opacity: 0.95,
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: 16,
|
||||||
|
color: themeMode === 'dark' ? '#fff' : '#23272f',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{activeTask.name}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{activeGroup && (
|
{activeGroup && (
|
||||||
<div className="group-drag-overlay">
|
<div
|
||||||
<div className="group-header-content">
|
style={{
|
||||||
<h3>{activeGroup.name}</h3>
|
background: themeMode === 'dark' ? '#23272f' : '#fff',
|
||||||
<span className="task-count">({activeGroup.tasks.length})</span>
|
borderRadius: 8,
|
||||||
</div>
|
boxShadow: '0 4px 16px rgba(0,0,0,0.12)',
|
||||||
|
padding: '16px 24px',
|
||||||
|
minWidth: 220,
|
||||||
|
maxWidth: 320,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 12,
|
||||||
|
opacity: 0.95,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h3 style={{ margin: 0, fontWeight: 600, fontSize: 18 }}>{activeGroup.name}</h3>
|
||||||
|
<span style={{ fontSize: 15, color: '#888' }}>({activeGroup.tasks.length})</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { Flex, Input, InputRef } from 'antd';
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
|
import { updateEnhancedKanbanTaskProgress } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
|
import { themeWiseColor } from '@/utils/themeWiseColor';
|
||||||
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
|
import { getCurrentGroup } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
|
import { useAuthService } from '@/hooks/useAuth';
|
||||||
|
import { ITaskCreateRequest } from '@/types/tasks/task-create-request.types';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { useSocket } from '@/socket/socketContext';
|
||||||
|
import { SocketEvents } from '@/shared/socket-events';
|
||||||
|
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||||
|
import logger from '@/utils/errorLogger';
|
||||||
|
|
||||||
|
type EnhancedKanbanCreateSubtaskCardProps = {
|
||||||
|
sectionId: string;
|
||||||
|
parentTaskId: string;
|
||||||
|
setShowNewSubtaskCard: (x: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EnhancedKanbanCreateSubtaskCard = ({
|
||||||
|
sectionId,
|
||||||
|
parentTaskId,
|
||||||
|
setShowNewSubtaskCard,
|
||||||
|
}: EnhancedKanbanCreateSubtaskCardProps) => {
|
||||||
|
const { socket, connected } = useSocket();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const [creatingTask, setCreatingTask] = useState<boolean>(false);
|
||||||
|
const [newSubtaskName, setNewSubtaskName] = useState<string>('');
|
||||||
|
const [isEnterKeyPressed, setIsEnterKeyPressed] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const cardRef = useRef<HTMLDivElement>(null);
|
||||||
|
const inputRef = useRef<InputRef>(null);
|
||||||
|
|
||||||
|
const { t } = useTranslation('kanban-board');
|
||||||
|
|
||||||
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
|
const { projectId } = useParams();
|
||||||
|
const currentSession = useAuthService().getCurrentSession();
|
||||||
|
|
||||||
|
const createRequestBody = (): ITaskCreateRequest | null => {
|
||||||
|
if (!projectId || !currentSession) return null;
|
||||||
|
const body: ITaskCreateRequest = {
|
||||||
|
project_id: projectId,
|
||||||
|
name: newSubtaskName,
|
||||||
|
reporter_id: currentSession.id,
|
||||||
|
team_id: currentSession.team_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupBy = getCurrentGroup();
|
||||||
|
if (groupBy === 'status') {
|
||||||
|
body.status_id = sectionId || undefined;
|
||||||
|
} else if (groupBy === 'priority') {
|
||||||
|
body.priority_id = sectionId || undefined;
|
||||||
|
} else if (groupBy === 'phase') {
|
||||||
|
body.phase_id = sectionId || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentTaskId) {
|
||||||
|
body.parent_task_id = parentTaskId;
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddSubtask = () => {
|
||||||
|
if (creatingTask || !projectId || !currentSession || newSubtaskName.trim() === '' || !connected)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setCreatingTask(true);
|
||||||
|
const body = createRequestBody();
|
||||||
|
if (!body) return;
|
||||||
|
|
||||||
|
socket?.emit(SocketEvents.QUICK_TASK.toString(), JSON.stringify(body));
|
||||||
|
socket?.once(SocketEvents.QUICK_TASK.toString(), (task: IProjectTask) => {
|
||||||
|
if (!task) return;
|
||||||
|
setCreatingTask(false);
|
||||||
|
setNewSubtaskName('');
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}, 0);
|
||||||
|
if (task.parent_task_id) {
|
||||||
|
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.parent_task_id);
|
||||||
|
socket?.once(SocketEvents.GET_TASK_PROGRESS.toString(), (data: {
|
||||||
|
id: string;
|
||||||
|
complete_ratio: number;
|
||||||
|
completed_count: number;
|
||||||
|
total_tasks_count: number;
|
||||||
|
parent_task: string;
|
||||||
|
}) => {
|
||||||
|
if (!data.parent_task) data.parent_task = task.parent_task_id || '';
|
||||||
|
dispatch(updateEnhancedKanbanTaskProgress({
|
||||||
|
id: task.id || '',
|
||||||
|
complete_ratio: data.complete_ratio,
|
||||||
|
completed_count: data.completed_count,
|
||||||
|
total_tasks_count: data.total_tasks_count,
|
||||||
|
parent_task: data.parent_task,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error adding task:', error);
|
||||||
|
setCreatingTask(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
setIsEnterKeyPressed(true);
|
||||||
|
handleAddSubtask();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputBlur = () => {
|
||||||
|
if (!isEnterKeyPressed && newSubtaskName.length > 0) {
|
||||||
|
handleAddSubtask();
|
||||||
|
}
|
||||||
|
setIsEnterKeyPressed(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelNewCard = (e: React.FocusEvent<HTMLDivElement>) => {
|
||||||
|
if (cardRef.current && !cardRef.current.contains(e.relatedTarget)) {
|
||||||
|
setNewSubtaskName('');
|
||||||
|
setShowNewSubtaskCard(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
ref={cardRef}
|
||||||
|
vertical
|
||||||
|
gap={4}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: 2,
|
||||||
|
backgroundColor: themeMode === 'dark' ? '#292929' : '#fafafa',
|
||||||
|
borderRadius: 6,
|
||||||
|
cursor: 'pointer',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
// className={`outline-1 ${themeWiseColor('outline-[#edeae9]', 'outline-[#6a696a]', themeMode)} hover:outline`}
|
||||||
|
onBlur={handleCancelNewCard}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
value={newSubtaskName}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
onChange={e => setNewSubtaskName(e.target.value)}
|
||||||
|
onKeyDown={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
setIsEnterKeyPressed(true);
|
||||||
|
handleAddSubtask();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyUp={e => e.stopPropagation()}
|
||||||
|
onKeyPress={e => e.stopPropagation()}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
|
placeholder={t('kanbanBoard.addSubTaskPlaceholder')}
|
||||||
|
className={`enhanced-kanban-create-subtask-input ${themeMode === 'dark' ? 'dark' : ''}`}
|
||||||
|
disabled={creatingTask}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EnhancedKanbanCreateSubtaskCard;
|
||||||
@@ -342,18 +342,6 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
|
||||||
align="center"
|
|
||||||
justify="center"
|
|
||||||
style={{
|
|
||||||
minWidth: 26,
|
|
||||||
height: 26,
|
|
||||||
borderRadius: 120,
|
|
||||||
backgroundColor: themeWiseColor('white', '#1e1e1e', themeMode),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{group.tasks.length}
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{isLoading && <LoadingOutlined style={{ color: colors.darkGray }} />}
|
{isLoading && <LoadingOutlined style={{ color: colors.darkGray }} />}
|
||||||
{isEditable ? (
|
{isEditable ? (
|
||||||
@@ -403,7 +391,7 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{name}
|
{name} ({group.tasks.length})
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
@@ -480,27 +468,35 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
<SortableContext items={taskIds} strategy={verticalListSortingStrategy}>
|
<SortableContext items={taskIds} strategy={verticalListSortingStrategy}>
|
||||||
{group.tasks.map((task, index) => (
|
{group.tasks.map((task, index) => (
|
||||||
<React.Fragment key={task.id}>
|
<React.Fragment key={task.id}>
|
||||||
{/* Show drop indicator before task if this is the target position */}
|
{/* Drop indicator before the card if this is the drop target */}
|
||||||
{shouldShowDropIndicators && overId === task.id && (
|
{overId === task.id && (
|
||||||
<div className="drop-preview-indicator">
|
<div
|
||||||
<div className="drop-line"></div>
|
style={{
|
||||||
</div>
|
height: 20,
|
||||||
|
background: themeMode === 'dark' ? '#444' : '#e0e0e0',
|
||||||
|
borderRadius: 4,
|
||||||
|
margin: '4px 0',
|
||||||
|
transition: 'background 0.2s',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<EnhancedKanbanTaskCard
|
<EnhancedKanbanTaskCard
|
||||||
task={task}
|
task={task}
|
||||||
sectionId={group.id}
|
sectionId={group.id}
|
||||||
isActive={task.id === activeTaskId}
|
isActive={task.id === activeTaskId}
|
||||||
isDropTarget={overId === task.id}
|
isDropTarget={overId === task.id}
|
||||||
/>
|
/>
|
||||||
|
{/* Drop indicator at the end if dropping at the end of the group */}
|
||||||
{/* Show drop indicator after last task if dropping at the end */}
|
{index === group.tasks.length - 1 && overId === group.id && (
|
||||||
{shouldShowDropIndicators &&
|
<div
|
||||||
index === group.tasks.length - 1 &&
|
style={{
|
||||||
overId === group.id && (
|
height: 12,
|
||||||
<div className="drop-preview-indicator">
|
background: themeMode === 'dark' ? '#444' : '#e0e0e0',
|
||||||
<div className="drop-line"></div>
|
borderRadius: 4,
|
||||||
</div>
|
margin: '8px 0',
|
||||||
|
transition: 'background 0.2s',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { ForkOutlined } from '@ant-design/icons';
|
|||||||
import { Dayjs } from 'dayjs';
|
import { Dayjs } from 'dayjs';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { CaretDownFilled, CaretRightFilled } from '@ant-design/icons';
|
import { CaretDownFilled, CaretRightFilled } from '@ant-design/icons';
|
||||||
import { fetchBoardSubTasks } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
import { fetchBoardSubTasks, toggleTaskExpansion } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
import { Divider } from 'antd';
|
import { Divider } from 'antd';
|
||||||
import { List } from 'antd';
|
import { List } from 'antd';
|
||||||
import { Skeleton } from 'antd';
|
import { Skeleton } from 'antd';
|
||||||
@@ -28,6 +28,7 @@ import { PlusOutlined } from '@ant-design/icons';
|
|||||||
import BoardSubTaskCard from '@/pages/projects/projectView/board/board-section/board-sub-task-card/board-sub-task-card';
|
import BoardSubTaskCard from '@/pages/projects/projectView/board/board-section/board-sub-task-card/board-sub-task-card';
|
||||||
import BoardCreateSubtaskCard from '@/pages/projects/projectView/board/board-section/board-sub-task-card/board-create-sub-task-card';
|
import BoardCreateSubtaskCard from '@/pages/projects/projectView/board/board-section/board-sub-task-card/board-create-sub-task-card';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import EnhancedKanbanCreateSubtaskCard from './EnhancedKanbanCreateSubtaskCard';
|
||||||
|
|
||||||
interface EnhancedKanbanTaskCardProps {
|
interface EnhancedKanbanTaskCardProps {
|
||||||
task: IProjectTask;
|
task: IProjectTask;
|
||||||
@@ -58,7 +59,7 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
|
|||||||
const [dueDate, setDueDate] = useState<Dayjs | null>(
|
const [dueDate, setDueDate] = useState<Dayjs | null>(
|
||||||
task?.end_date ? dayjs(task?.end_date) : null
|
task?.end_date ? dayjs(task?.end_date) : null
|
||||||
);
|
);
|
||||||
const [isSubTaskShow, setIsSubTaskShow] = useState(false);
|
|
||||||
const projectId = useAppSelector(state => state.projectReducer.projectId);
|
const projectId = useAppSelector(state => state.projectReducer.projectId);
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
@@ -115,13 +116,17 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
|
|||||||
|
|
||||||
const handleSubTaskExpand = useCallback(() => {
|
const handleSubTaskExpand = useCallback(() => {
|
||||||
if (task && task.id && projectId) {
|
if (task && task.id && projectId) {
|
||||||
if (task.show_sub_tasks) {
|
// Check if subtasks are already loaded and we have subtask data
|
||||||
|
if (task.sub_tasks && task.sub_tasks.length > 0 && task.sub_tasks_count > 0) {
|
||||||
// If subtasks are already loaded, just toggle visibility
|
// If subtasks are already loaded, just toggle visibility
|
||||||
setIsSubTaskShow(prev => !prev);
|
dispatch(toggleTaskExpansion(task.id));
|
||||||
} else {
|
} else if (task.sub_tasks_count > 0) {
|
||||||
// If subtasks need to be fetched, show the section first with loading state
|
// If we have a subtask count but no loaded subtasks, fetch them
|
||||||
setIsSubTaskShow(true);
|
dispatch(toggleTaskExpansion(task.id));
|
||||||
dispatch(fetchBoardSubTasks({ taskId: task.id, projectId }));
|
dispatch(fetchBoardSubTasks({ taskId: task.id, projectId }));
|
||||||
|
} else {
|
||||||
|
// If no subtasks exist, just toggle visibility (will show empty state)
|
||||||
|
dispatch(toggleTaskExpansion(task.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [task, projectId, dispatch]);
|
}, [task, projectId, dispatch]);
|
||||||
@@ -199,13 +204,13 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
|
|||||||
>
|
>
|
||||||
<ForkOutlined rotate={90} />
|
<ForkOutlined rotate={90} />
|
||||||
<span>{task.sub_tasks_count}</span>
|
<span>{task.sub_tasks_count}</span>
|
||||||
{isSubTaskShow ? <CaretDownFilled /> : <CaretRightFilled />}
|
{task.show_sub_tasks ? <CaretDownFilled /> : <CaretRightFilled />}
|
||||||
</Tag>
|
</Tag>
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex vertical gap={8}>
|
<Flex vertical gap={8}>
|
||||||
{isSubTaskShow && (
|
{task.show_sub_tasks && (
|
||||||
<Flex vertical>
|
<Flex vertical>
|
||||||
<Divider style={{ marginBlock: 0 }} />
|
<Divider style={{ marginBlock: 0 }} />
|
||||||
<List>
|
<List>
|
||||||
@@ -215,13 +220,21 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
|
|||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!task.sub_tasks_loading && task?.sub_tasks &&
|
{!task.sub_tasks_loading && task?.sub_tasks && task.sub_tasks.length > 0 &&
|
||||||
task?.sub_tasks.map((subtask: any) => (
|
task.sub_tasks.map((subtask: any) => (
|
||||||
<BoardSubTaskCard key={subtask.id} subtask={subtask} sectionId={sectionId} />
|
<BoardSubTaskCard key={subtask.id} subtask={subtask} sectionId={sectionId} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{!task.sub_tasks_loading && (!task?.sub_tasks || task.sub_tasks.length === 0) && task.sub_tasks_count === 0 && (
|
||||||
|
<List.Item>
|
||||||
|
<div style={{ padding: '8px 0', color: '#999', fontSize: '12px' }}>
|
||||||
|
{t('noSubtasks', 'No subtasks')}
|
||||||
|
</div>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
{showNewSubtaskCard && (
|
{showNewSubtaskCard && (
|
||||||
<BoardCreateSubtaskCard
|
<EnhancedKanbanCreateSubtaskCard
|
||||||
sectionId={sectionId}
|
sectionId={sectionId}
|
||||||
parentTaskId={task.id || ''}
|
parentTaskId={task.id || ''}
|
||||||
setShowNewSubtaskCard={setShowNewSubtaskCard}
|
setShowNewSubtaskCard={setShowNewSubtaskCard}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { setTaskAssignee } from '@/features/task-drawer/task-drawer.slice';
|
|||||||
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
||||||
import { updateTaskAssignees as updateBoardTaskAssignees } from '@/features/board/board-slice';
|
import { updateTaskAssignees as updateBoardTaskAssignees } from '@/features/board/board-slice';
|
||||||
import { updateTaskAssignees as updateTasksListTaskAssignees } from '@/features/tasks/tasks.slice';
|
import { updateTaskAssignees as updateTasksListTaskAssignees } from '@/features/tasks/tasks.slice';
|
||||||
|
import { updateEnhancedKanbanTaskAssignees } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
interface TaskDrawerAssigneeSelectorProps {
|
interface TaskDrawerAssigneeSelectorProps {
|
||||||
task: ITaskViewModel;
|
task: ITaskViewModel;
|
||||||
}
|
}
|
||||||
@@ -88,12 +89,12 @@ const TaskDrawerAssigneeSelector = ({ task }: TaskDrawerAssigneeSelectorProps) =
|
|||||||
SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(),
|
SocketEvents.QUICK_ASSIGNEES_UPDATE.toString(),
|
||||||
(data: ITaskAssigneesUpdateResponse) => {
|
(data: ITaskAssigneesUpdateResponse) => {
|
||||||
dispatch(setTaskAssignee(data));
|
dispatch(setTaskAssignee(data));
|
||||||
// if (tab === 'tasks-list') {
|
if (tab === 'tasks-list') {
|
||||||
// dispatch(updateTasksListTaskAssignees(data));
|
dispatch(updateTasksListTaskAssignees(data));
|
||||||
// }
|
}
|
||||||
// if (tab === 'board') {
|
if (tab === 'board') {
|
||||||
// dispatch(updateBoardTaskAssignees(data));
|
dispatch(updateEnhancedKanbanTaskAssignees(data));
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { ITaskViewModel } from '@/types/tasks/task.types';
|
|||||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import { setStartDate, setTaskEndDate } from '@/features/task-drawer/task-drawer.slice';
|
import { setStartDate, setTaskEndDate } from '@/features/task-drawer/task-drawer.slice';
|
||||||
|
import { updateEnhancedKanbanTaskStartDate, updateEnhancedKanbanTaskEndDate } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
|
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
||||||
interface TaskDrawerDueDateProps {
|
interface TaskDrawerDueDateProps {
|
||||||
task: ITaskViewModel;
|
task: ITaskViewModel;
|
||||||
t: TFunction;
|
t: TFunction;
|
||||||
@@ -24,6 +26,7 @@ const TaskDrawerDueDate = ({ task, t, form }: TaskDrawerDueDateProps) => {
|
|||||||
const { socket } = useSocket();
|
const { socket } = useSocket();
|
||||||
const [isShowStartDate, setIsShowStartDate] = useState(false);
|
const [isShowStartDate, setIsShowStartDate] = useState(false);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const { tab } = useTabSearchParam();
|
||||||
// Date handling
|
// Date handling
|
||||||
const startDayjs = task?.start_date ? dayjs(task.start_date) : null;
|
const startDayjs = task?.start_date ? dayjs(task.start_date) : null;
|
||||||
const dueDayjs = task?.end_date ? dayjs(task.end_date) : null;
|
const dueDayjs = task?.end_date ? dayjs(task.end_date) : null;
|
||||||
@@ -63,6 +66,10 @@ const TaskDrawerDueDate = ({ task, t, form }: TaskDrawerDueDateProps) => {
|
|||||||
(data: IProjectTask) => {
|
(data: IProjectTask) => {
|
||||||
dispatch(setStartDate(data));
|
dispatch(setStartDate(data));
|
||||||
|
|
||||||
|
// Also update enhanced kanban if on board tab
|
||||||
|
if (tab === 'board') {
|
||||||
|
dispatch(updateEnhancedKanbanTaskStartDate({ task: data }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -88,6 +95,10 @@ const TaskDrawerDueDate = ({ task, t, form }: TaskDrawerDueDateProps) => {
|
|||||||
(data: IProjectTask) => {
|
(data: IProjectTask) => {
|
||||||
dispatch(setTaskEndDate(data));
|
dispatch(setTaskEndDate(data));
|
||||||
|
|
||||||
|
// Also update enhanced kanban if on board tab
|
||||||
|
if (tab === 'board') {
|
||||||
|
dispatch(updateEnhancedKanbanTaskEndDate({ task: data }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { useAppDispatch } from '@/hooks/useAppDispatch';
|
|||||||
import { setTaskLabels } from '@/features/task-drawer/task-drawer.slice';
|
import { setTaskLabels } from '@/features/task-drawer/task-drawer.slice';
|
||||||
import { setLabels, updateTaskLabel } from '@/features/tasks/tasks.slice';
|
import { setLabels, updateTaskLabel } from '@/features/tasks/tasks.slice';
|
||||||
import { setBoardLabels, updateBoardTaskLabel } from '@/features/board/board-slice';
|
import { setBoardLabels, updateBoardTaskLabel } from '@/features/board/board-slice';
|
||||||
|
import { updateEnhancedKanbanTaskLabels } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
import { ILabelsChangeResponse } from '@/types/tasks/taskList.types';
|
import { ILabelsChangeResponse } from '@/types/tasks/taskList.types';
|
||||||
import { ITaskLabelFilter } from '@/types/tasks/taskLabel.types';
|
import { ITaskLabelFilter } from '@/types/tasks/taskLabel.types';
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ const TaskDrawerLabels = ({ task, t }: TaskDrawerLabelsProps) => {
|
|||||||
dispatch(updateTaskLabel(data));
|
dispatch(updateTaskLabel(data));
|
||||||
}
|
}
|
||||||
if (tab === 'board') {
|
if (tab === 'board') {
|
||||||
dispatch(updateBoardTaskLabel(data));
|
dispatch(updateEnhancedKanbanTaskLabels(data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -91,7 +92,7 @@ const TaskDrawerLabels = ({ task, t }: TaskDrawerLabelsProps) => {
|
|||||||
dispatch(updateTaskLabel(data));
|
dispatch(updateTaskLabel(data));
|
||||||
}
|
}
|
||||||
if (tab === 'board') {
|
if (tab === 'board') {
|
||||||
dispatch(updateBoardTaskLabel(data));
|
dispatch(updateEnhancedKanbanTaskLabels(data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { ITaskListPriorityChangeResponse } from '@/types/tasks/task-list-priorit
|
|||||||
import { setTaskPriority } from '@/features/task-drawer/task-drawer.slice';
|
import { setTaskPriority } from '@/features/task-drawer/task-drawer.slice';
|
||||||
import { updateTaskPriority as updateBoardTaskPriority } from '@/features/board/board-slice';
|
import { updateTaskPriority as updateBoardTaskPriority } from '@/features/board/board-slice';
|
||||||
import { updateTaskPriority as updateTasksListTaskPriority } from '@/features/tasks/tasks.slice';
|
import { updateTaskPriority as updateTasksListTaskPriority } from '@/features/tasks/tasks.slice';
|
||||||
|
import { updateEnhancedKanbanTaskPriority } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
|
|
||||||
type PriorityDropdownProps = {
|
type PriorityDropdownProps = {
|
||||||
task: ITaskViewModel;
|
task: ITaskViewModel;
|
||||||
@@ -48,7 +49,7 @@ const PriorityDropdown = ({ task }: PriorityDropdownProps) => {
|
|||||||
dispatch(updateTasksListTaskPriority(data));
|
dispatch(updateTasksListTaskPriority(data));
|
||||||
}
|
}
|
||||||
if (tab === 'board') {
|
if (tab === 'board') {
|
||||||
dispatch(updateBoardTaskPriority(data));
|
dispatch(updateEnhancedKanbanTaskPriority(data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { setTaskStatus } from '@/features/task-drawer/task-drawer.slice';
|
|||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import { updateBoardTaskStatus } from '@/features/board/board-slice';
|
import { updateBoardTaskStatus } from '@/features/board/board-slice';
|
||||||
import { updateTaskProgress, updateTaskStatus } from '@/features/tasks/tasks.slice';
|
import { updateTaskProgress, updateTaskStatus } from '@/features/tasks/tasks.slice';
|
||||||
|
import { updateEnhancedKanbanTaskStatus, updateEnhancedKanbanTaskProgress } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
||||||
|
|
||||||
interface TaskDrawerProgressProps {
|
interface TaskDrawerProgressProps {
|
||||||
@@ -102,6 +103,7 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
socket?.once(SocketEvents.GET_TASK_PROGRESS.toString(), (data: any) => {
|
socket?.once(SocketEvents.GET_TASK_PROGRESS.toString(), (data: any) => {
|
||||||
|
if (tab === 'tasks-list') {
|
||||||
dispatch(
|
dispatch(
|
||||||
updateTaskProgress({
|
updateTaskProgress({
|
||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
@@ -110,6 +112,18 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
|
|||||||
completedCount: data.completed_count,
|
completedCount: data.completed_count,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
if (tab === 'board') {
|
||||||
|
dispatch(
|
||||||
|
updateEnhancedKanbanTaskProgress({
|
||||||
|
id: task.id,
|
||||||
|
complete_ratio: data.complete_ratio,
|
||||||
|
completed_count: data.completed_count,
|
||||||
|
total_tasks_count: data.total_tasks_count,
|
||||||
|
parent_task: task.parent_task_id || null,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (task.id) {
|
if (task.id) {
|
||||||
@@ -185,7 +199,7 @@ const TaskDrawerProgress = ({ task, form }: TaskDrawerProgressProps) => {
|
|||||||
dispatch(updateTaskStatus(data));
|
dispatch(updateTaskStatus(data));
|
||||||
}
|
}
|
||||||
if (tab === 'board') {
|
if (tab === 'board') {
|
||||||
dispatch(updateBoardTaskStatus(data));
|
dispatch(updateEnhancedKanbanTaskStatus(data));
|
||||||
}
|
}
|
||||||
if (data.parent_task)
|
if (data.parent_task)
|
||||||
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), data.parent_task);
|
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), data.parent_task);
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ const SubTaskTable = ({ subTasks, loadingSubTasks, refreshSubTasks, t }: SubTask
|
|||||||
if (task.parent_task_id) {
|
if (task.parent_task_id) {
|
||||||
refreshSubTasks();
|
refreshSubTasks();
|
||||||
dispatch(updateSubtask({ sectionId: '', subtask: task, mode: 'add' }));
|
dispatch(updateSubtask({ sectionId: '', subtask: task, mode: 'add' }));
|
||||||
|
|
||||||
|
// Note: Enhanced kanban updates are now handled by the global socket handler
|
||||||
|
// No need to dispatch here as it will be handled by useTaskSocketHandlers
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -109,6 +112,10 @@ const SubTaskTable = ({ subTasks, loadingSubTasks, refreshSubTasks, t }: SubTask
|
|||||||
try {
|
try {
|
||||||
await tasksApiService.deleteTask(taskId);
|
await tasksApiService.deleteTask(taskId);
|
||||||
dispatch(updateSubtask({ sectionId: '', subtask: { id: taskId, parent_task_id: selectedTaskId || '' }, mode: 'delete' }));
|
dispatch(updateSubtask({ sectionId: '', subtask: { id: taskId, parent_task_id: selectedTaskId || '' }, mode: 'delete' }));
|
||||||
|
|
||||||
|
// Note: Enhanced kanban updates are now handled by the global socket handler
|
||||||
|
// No need to dispatch here as it will be handled by useTaskSocketHandlers
|
||||||
|
|
||||||
refreshSubTasks();
|
refreshSubTasks();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error deleting subtask:', error);
|
logger.error('Error deleting subtask:', error);
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ import { SocketEvents } from '@/shared/socket-events';
|
|||||||
import useTaskDrawerUrlSync from '@/hooks/useTaskDrawerUrlSync';
|
import useTaskDrawerUrlSync from '@/hooks/useTaskDrawerUrlSync';
|
||||||
import { deleteTask } from '@/features/tasks/tasks.slice';
|
import { deleteTask } from '@/features/tasks/tasks.slice';
|
||||||
import { deleteBoardTask, updateTaskName } from '@/features/board/board-slice';
|
import { deleteBoardTask, updateTaskName } from '@/features/board/board-slice';
|
||||||
|
import { updateEnhancedKanbanTaskName } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
|
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
||||||
|
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||||
|
|
||||||
type TaskDrawerHeaderProps = {
|
type TaskDrawerHeaderProps = {
|
||||||
inputRef: React.RefObject<InputRef | null>;
|
inputRef: React.RefObject<InputRef | null>;
|
||||||
@@ -26,6 +29,7 @@ const TaskDrawerHeader = ({ inputRef, t }: TaskDrawerHeaderProps) => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { socket, connected } = useSocket();
|
const { socket, connected } = useSocket();
|
||||||
const { clearTaskFromUrl } = useTaskDrawerUrlSync();
|
const { clearTaskFromUrl } = useTaskDrawerUrlSync();
|
||||||
|
const { tab } = useTabSearchParam();
|
||||||
const isDeleting = useRef(false);
|
const isDeleting = useRef(false);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
@@ -84,7 +88,13 @@ const TaskDrawerHeader = ({ inputRef, t }: TaskDrawerHeaderProps) => {
|
|||||||
|
|
||||||
const handleReceivedTaskNameChange = (data: { id: string; parent_task: string; name: string }) => {
|
const handleReceivedTaskNameChange = (data: { id: string; parent_task: string; name: string }) => {
|
||||||
if (data.id === selectedTaskId) {
|
if (data.id === selectedTaskId) {
|
||||||
dispatch(updateTaskName({ task: data }));
|
const taskData = { ...data, manual_progress: false } as IProjectTask;
|
||||||
|
dispatch(updateTaskName({ task: taskData }));
|
||||||
|
|
||||||
|
// Also update enhanced kanban if on board tab
|
||||||
|
if (tab === 'board') {
|
||||||
|
dispatch(updateEnhancedKanbanTaskName({ task: taskData }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -152,7 +162,7 @@ const TaskDrawerHeader = ({ inputRef, t }: TaskDrawerHeaderProps) => {
|
|||||||
|
|
||||||
<TaskDrawerStatusDropdown
|
<TaskDrawerStatusDropdown
|
||||||
statuses={taskFormViewModel?.statuses ?? []}
|
statuses={taskFormViewModel?.statuses ?? []}
|
||||||
task={taskFormViewModel?.task ?? {}}
|
task={taskFormViewModel?.task ?? {} as ITaskViewModel}
|
||||||
teamId={currentSession?.team_id ?? ''}
|
teamId={currentSession?.team_id ?? ''}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react';
|
import React, { useMemo, useCallback, useState, useEffect } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -13,17 +13,11 @@ import {
|
|||||||
import {
|
import {
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
CloseOutlined,
|
CloseOutlined,
|
||||||
MoreOutlined,
|
|
||||||
RetweetOutlined,
|
RetweetOutlined,
|
||||||
UserAddOutlined,
|
UserAddOutlined,
|
||||||
InboxOutlined,
|
InboxOutlined,
|
||||||
TagsOutlined,
|
TagsOutlined,
|
||||||
UsergroupAddOutlined,
|
UsergroupAddOutlined,
|
||||||
CheckOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
CopyOutlined,
|
|
||||||
ExportOutlined,
|
|
||||||
CalendarOutlined,
|
|
||||||
FlagOutlined,
|
FlagOutlined,
|
||||||
BulbOutlined
|
BulbOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import React, { useMemo, useCallback, useEffect, useRef } from 'react';
|
import React, { useMemo, useCallback, useEffect, useRef } from 'react';
|
||||||
import { FixedSizeList as List } from 'react-window';
|
import { FixedSizeList as List } from 'react-window';
|
||||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Empty } from 'antd';
|
import { Empty, Button } from 'antd';
|
||||||
|
import { RightOutlined, DownOutlined } from '@ant-design/icons';
|
||||||
import { taskManagementSelectors } from '@/features/task-management/task-management.slice';
|
import { taskManagementSelectors } from '@/features/task-management/task-management.slice';
|
||||||
|
import { toggleGroupCollapsed } from '@/features/task-management/grouping.slice';
|
||||||
import { Task } from '@/types/task-management.types';
|
import { Task } from '@/types/task-management.types';
|
||||||
import TaskRow from './task-row';
|
import TaskRow from './task-row';
|
||||||
import AddTaskListRow from '@/pages/projects/projectView/taskList/task-list-table/task-list-table-rows/add-task-list-row';
|
import AddTaskListRow from '@/pages/projects/projectView/taskList/task-list-table/task-list-table-rows/add-task-list-row';
|
||||||
@@ -35,6 +37,7 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
width,
|
width,
|
||||||
tasksById
|
tasksById
|
||||||
}) => {
|
}) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
const { t } = useTranslation('task-management');
|
const { t } = useTranslation('task-management');
|
||||||
|
|
||||||
// Get theme from Redux store
|
// Get theme from Redux store
|
||||||
@@ -43,6 +46,10 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
// Get field visibility from taskListFields slice
|
// Get field visibility from taskListFields slice
|
||||||
const taskListFields = useSelector((state: RootState) => state.taskManagementFields) as TaskListField[];
|
const taskListFields = useSelector((state: RootState) => state.taskManagementFields) as TaskListField[];
|
||||||
|
|
||||||
|
// Get group collapse state from Redux
|
||||||
|
const groupStates = useSelector((state: RootState) => state.grouping.groupStates);
|
||||||
|
const isCollapsed = groupStates[group.id]?.collapsed || false;
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Improved virtualization for better user experience
|
// PERFORMANCE OPTIMIZATION: Improved virtualization for better user experience
|
||||||
const VIRTUALIZATION_THRESHOLD = 25; // Increased threshold - virtualize when there are more tasks
|
const VIRTUALIZATION_THRESHOLD = 25; // Increased threshold - virtualize when there are more tasks
|
||||||
const TASK_ROW_HEIGHT = 40;
|
const TASK_ROW_HEIGHT = 40;
|
||||||
@@ -52,7 +59,12 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Batch rendering to prevent long tasks
|
// PERFORMANCE OPTIMIZATION: Batch rendering to prevent long tasks
|
||||||
const RENDER_BATCH_SIZE = 5; // Render max 5 tasks per frame
|
const RENDER_BATCH_SIZE = 5; // Render max 5 tasks per frame
|
||||||
const FRAME_BUDGET_MS = 8; // Leave 8ms per frame for other operations
|
const FRAME_BUDGET_MS = 8;
|
||||||
|
|
||||||
|
// Handle collapse/expand toggle
|
||||||
|
const handleToggleCollapse = useCallback(() => {
|
||||||
|
dispatch(toggleGroupCollapsed(group.id));
|
||||||
|
}, [dispatch, group.id]);
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Add early return for empty groups
|
// PERFORMANCE OPTIMIZATION: Add early return for empty groups
|
||||||
if (!group || !group.taskIds || group.taskIds.length === 0) {
|
if (!group || !group.taskIds || group.taskIds.length === 0) {
|
||||||
@@ -84,6 +96,22 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
// No margin - header should overlap the sticky border
|
// No margin - header should overlap the sticky border
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={isCollapsed ? <RightOutlined /> : <DownOutlined />}
|
||||||
|
onClick={handleToggleCollapse}
|
||||||
|
className="task-group-collapse-button"
|
||||||
|
style={{
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
background: 'transparent',
|
||||||
|
padding: '4px',
|
||||||
|
marginRight: '8px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<span className="task-group-header-text">
|
<span className="task-group-header-text">
|
||||||
{group?.title || 'Empty Group'} (0)
|
{group?.title || 'Empty Group'} (0)
|
||||||
</span>
|
</span>
|
||||||
@@ -92,7 +120,7 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Column Headers */}
|
{/* Column Headers */}
|
||||||
<div className="task-group-column-headers" style={{
|
<div style={{
|
||||||
marginLeft: '4px', // Account for sticky border
|
marginLeft: '4px', // Account for sticky border
|
||||||
height: COLUMN_HEADER_HEIGHT,
|
height: COLUMN_HEADER_HEIGHT,
|
||||||
background: 'var(--task-bg-secondary, #f5f5f5)',
|
background: 'var(--task-bg-secondary, #f5f5f5)',
|
||||||
@@ -141,35 +169,22 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Get tasks for this group using direct lookup (no mapping/filtering)
|
// Get tasks for this group using memoization for performance
|
||||||
const groupTasks = useMemo(() => {
|
const groupTasks = useMemo(() => {
|
||||||
// PERFORMANCE OPTIMIZATION: Use for loop instead of map for better performance
|
return group.taskIds
|
||||||
const tasks: Task[] = [];
|
.map((taskId: string) => tasksById[taskId])
|
||||||
for (let i = 0; i < group.taskIds.length; i++) {
|
.filter((task: Task | undefined): task is Task => task !== undefined);
|
||||||
const task = tasksById[group.taskIds[i]];
|
|
||||||
if (task) {
|
|
||||||
tasks.push(task);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tasks;
|
|
||||||
}, [group.taskIds, tasksById]);
|
}, [group.taskIds, tasksById]);
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Only calculate selection state when needed
|
// Calculate selection state for the group checkbox
|
||||||
const selectionState = useMemo(() => {
|
const selectionState = useMemo(() => {
|
||||||
if (groupTasks.length === 0) {
|
if (groupTasks.length === 0) {
|
||||||
return { isAllSelected: false, isIndeterminate: false };
|
return { isAllSelected: false, isIndeterminate: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// PERFORMANCE OPTIMIZATION: Use for loop instead of filter for better performance
|
const selectedTasksInGroup = groupTasks.filter((task: Task) => selectedTaskIds.includes(task.id));
|
||||||
let selectedCount = 0;
|
const isAllSelected = selectedTasksInGroup.length === groupTasks.length;
|
||||||
for (let i = 0; i < groupTasks.length; i++) {
|
const isIndeterminate = selectedTasksInGroup.length > 0 && selectedTasksInGroup.length < groupTasks.length;
|
||||||
if (selectedTaskIds.includes(groupTasks[i].id)) {
|
|
||||||
selectedCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAllSelected = selectedCount === groupTasks.length;
|
|
||||||
const isIndeterminate = selectedCount > 0 && selectedCount < groupTasks.length;
|
|
||||||
|
|
||||||
return { isAllSelected, isIndeterminate };
|
return { isAllSelected, isIndeterminate };
|
||||||
}, [groupTasks, selectedTaskIds]);
|
}, [groupTasks, selectedTaskIds]);
|
||||||
@@ -339,6 +354,69 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
};
|
};
|
||||||
}, [handleScroll]);
|
}, [handleScroll]);
|
||||||
|
|
||||||
|
// If group is collapsed, show only header
|
||||||
|
if (isCollapsed) {
|
||||||
|
return (
|
||||||
|
<div className="virtualized-task-list collapsed" style={{ height: HEADER_HEIGHT, position: 'relative' }}>
|
||||||
|
{/* Sticky Group Color Border */}
|
||||||
|
<div
|
||||||
|
className="sticky-group-border"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: '4px',
|
||||||
|
backgroundColor: group.color || '#f0f0f0',
|
||||||
|
zIndex: 15,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Group Header */}
|
||||||
|
<div className="task-group-header" style={{ height: HEADER_HEIGHT }}>
|
||||||
|
<div className="task-group-header-row">
|
||||||
|
<div
|
||||||
|
className="task-group-header-content"
|
||||||
|
style={{
|
||||||
|
backgroundColor: group.color || '#f0f0f0',
|
||||||
|
// No margin - header should overlap the sticky border
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={isCollapsed ? <RightOutlined /> : <DownOutlined />}
|
||||||
|
onClick={handleToggleCollapse}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleToggleCollapse();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="task-group-collapse-button"
|
||||||
|
aria-label={isCollapsed ? 'Expand group' : 'Collapse group'}
|
||||||
|
title={isCollapsed ? 'Click to expand group' : 'Click to collapse group'}
|
||||||
|
style={{
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
background: 'transparent',
|
||||||
|
padding: '4px',
|
||||||
|
marginRight: '8px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="task-group-header-text">
|
||||||
|
{group.title} ({groupTasks.length})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="virtualized-task-list" style={{ height: groupHeight, position: 'relative' }}>
|
<div className="virtualized-task-list" style={{ height: groupHeight, position: 'relative' }}>
|
||||||
{/* Sticky Group Color Border */}
|
{/* Sticky Group Color Border */}
|
||||||
@@ -366,6 +444,30 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
// No margin - header should overlap the sticky border
|
// No margin - header should overlap the sticky border
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={isCollapsed ? <RightOutlined /> : <DownOutlined />}
|
||||||
|
onClick={handleToggleCollapse}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleToggleCollapse();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="task-group-collapse-button"
|
||||||
|
aria-label={isCollapsed ? 'Expand group' : 'Collapse group'}
|
||||||
|
title={isCollapsed ? 'Click to expand group' : 'Click to collapse group'}
|
||||||
|
style={{
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
background: 'transparent',
|
||||||
|
padding: '4px',
|
||||||
|
marginRight: '8px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<span className="task-group-header-text">
|
<span className="task-group-header-text">
|
||||||
{group.title} ({groupTasks.length})
|
{group.title} ({groupTasks.length})
|
||||||
</span>
|
</span>
|
||||||
@@ -586,6 +688,24 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = React.memo(({
|
|||||||
font-weight: 600 !important;
|
font-weight: 600 !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
/* Collapse button styles */
|
||||||
|
.task-group-collapse-button {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
.task-group-collapse-button:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15) !important;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
.task-group-collapse-button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
.task-group-collapse-button .anticon {
|
||||||
|
font-size: 12px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
/* Column headers styles */
|
/* Column headers styles */
|
||||||
.task-table-header-cell {
|
.task-table-header-cell {
|
||||||
background: var(--task-bg-secondary, #f5f5f5);
|
background: var(--task-bg-secondary, #f5f5f5);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
ITaskListSortableColumn,
|
ITaskListSortableColumn,
|
||||||
} from '@/types/tasks/taskList.types';
|
} from '@/types/tasks/taskList.types';
|
||||||
import { tasksApiService } from '@/api/tasks/tasks.api.service';
|
import { tasksApiService } from '@/api/tasks/tasks.api.service';
|
||||||
|
import { subTasksApiService } from '@/api/tasks/subtasks.api.service';
|
||||||
import logger from '@/utils/errorLogger';
|
import logger from '@/utils/errorLogger';
|
||||||
import { ITaskListMemberFilter } from '@/types/tasks/taskListFilters.types';
|
import { ITaskListMemberFilter } from '@/types/tasks/taskListFilters.types';
|
||||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||||
@@ -14,6 +15,10 @@ import { ITaskListStatusChangeResponse } from '@/types/tasks/task-list-status.ty
|
|||||||
import { ITaskListPriorityChangeResponse } from '@/types/tasks/task-list-priority.types';
|
import { ITaskListPriorityChangeResponse } from '@/types/tasks/task-list-priority.types';
|
||||||
import { ITaskLabelFilter } from '@/types/tasks/taskLabel.types';
|
import { ITaskLabelFilter } from '@/types/tasks/taskLabel.types';
|
||||||
import { labelsApiService } from '@/api/taskAttributes/labels/labels.api.service';
|
import { labelsApiService } from '@/api/taskAttributes/labels/labels.api.service';
|
||||||
|
import { ITaskAssigneesUpdateResponse } from '@/types/tasks/task-assignee-update-response';
|
||||||
|
import { ITaskAssignee } from '@/types/project/projectTasksViewModel.types';
|
||||||
|
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
|
||||||
|
import { ILabelsChangeResponse } from '@/types/tasks/taskList.types';
|
||||||
|
|
||||||
export enum IGroupBy {
|
export enum IGroupBy {
|
||||||
STATUS = 'status',
|
STATUS = 'status',
|
||||||
@@ -276,43 +281,9 @@ export const fetchBoardSubTasks = createAsyncThunk(
|
|||||||
{ rejectWithValue, getState }
|
{ rejectWithValue, getState }
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const state = getState() as { enhancedKanbanReducer: EnhancedKanbanState };
|
// Use the dedicated subtasks API endpoint
|
||||||
const { enhancedKanbanReducer } = state;
|
const response = await subTasksApiService.getSubTasks(taskId);
|
||||||
|
return response.body || [];
|
||||||
// Check if the task is already expanded (optional, can be enhanced later)
|
|
||||||
// const task = enhancedKanbanReducer.taskGroups.flatMap(group => group.tasks).find(t => t.id === taskId);
|
|
||||||
// if (task?.show_sub_tasks) {
|
|
||||||
// return [];
|
|
||||||
// }
|
|
||||||
|
|
||||||
const selectedMembers = enhancedKanbanReducer.taskAssignees
|
|
||||||
.filter(member => member.selected)
|
|
||||||
.map(member => member.id)
|
|
||||||
.join(' ');
|
|
||||||
|
|
||||||
const selectedLabels = enhancedKanbanReducer.labels
|
|
||||||
.filter(label => label.selected)
|
|
||||||
.map(label => label.id)
|
|
||||||
.join(' ');
|
|
||||||
|
|
||||||
const config: ITaskListConfigV2 = {
|
|
||||||
id: projectId,
|
|
||||||
archived: enhancedKanbanReducer.archived,
|
|
||||||
group: enhancedKanbanReducer.groupBy,
|
|
||||||
field: enhancedKanbanReducer.fields.map(field => `${field.key} ${field.sort_order}`).join(','),
|
|
||||||
order: '',
|
|
||||||
search: enhancedKanbanReducer.search || '',
|
|
||||||
statuses: '',
|
|
||||||
members: selectedMembers,
|
|
||||||
projects: '',
|
|
||||||
isSubtasksInclude: false,
|
|
||||||
labels: selectedLabels,
|
|
||||||
priorities: enhancedKanbanReducer.priorities.join(' '),
|
|
||||||
parent_task: taskId,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await tasksApiService.getTaskList(config);
|
|
||||||
return response.body;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Fetch Enhanced Board Sub Tasks', error);
|
logger.error('Fetch Enhanced Board Sub Tasks', error);
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
@@ -357,6 +328,53 @@ export const fetchEnhancedKanbanLabels = createAsyncThunk(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Helper functions for common operations (similar to board-slice.ts)
|
||||||
|
const findTaskInAllGroups = (
|
||||||
|
taskGroups: ITaskListGroup[],
|
||||||
|
taskId: string
|
||||||
|
): { task: IProjectTask; group: ITaskListGroup; groupId: string } | null => {
|
||||||
|
for (const group of taskGroups) {
|
||||||
|
const task = group.tasks.find(t => t.id === taskId);
|
||||||
|
if (task) return { task, group, groupId: group.id };
|
||||||
|
|
||||||
|
// Check in subtasks
|
||||||
|
for (const parentTask of group.tasks) {
|
||||||
|
if (!parentTask.sub_tasks) continue;
|
||||||
|
const subtask = parentTask.sub_tasks.find(st => st.id === taskId);
|
||||||
|
if (subtask) return { task: subtask, group, groupId: group.id };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteTaskFromGroup = (
|
||||||
|
taskGroups: ITaskListGroup[],
|
||||||
|
task: IProjectTask,
|
||||||
|
groupId: string,
|
||||||
|
index: number | null = null
|
||||||
|
): void => {
|
||||||
|
const group = taskGroups.find(g => g.id === groupId);
|
||||||
|
if (!group || !task.id) return;
|
||||||
|
|
||||||
|
if (task.is_sub_task) {
|
||||||
|
const parentTask = group.tasks.find(t => t.id === task.parent_task_id);
|
||||||
|
if (parentTask) {
|
||||||
|
const subTaskIndex = parentTask.sub_tasks?.findIndex(t => t.id === task.id);
|
||||||
|
if (typeof subTaskIndex !== 'undefined' && subTaskIndex !== -1) {
|
||||||
|
parentTask.sub_tasks_count = Math.max((parentTask.sub_tasks_count || 0) - 1, 0);
|
||||||
|
parentTask.sub_tasks?.splice(subTaskIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const taskIndex = index ?? group.tasks.findIndex(t => t.id === task.id);
|
||||||
|
if (taskIndex !== -1) {
|
||||||
|
group.tasks.splice(taskIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const enhancedKanbanSlice = createSlice({
|
const enhancedKanbanSlice = createSlice({
|
||||||
name: 'enhancedKanbanReducer',
|
name: 'enhancedKanbanReducer',
|
||||||
initialState,
|
initialState,
|
||||||
@@ -497,7 +515,7 @@ const enhancedKanbanSlice = createSlice({
|
|||||||
|
|
||||||
// Enhanced Kanban external status update (for use in task drawer dropdown)
|
// Enhanced Kanban external status update (for use in task drawer dropdown)
|
||||||
updateEnhancedKanbanTaskStatus: (state, action: PayloadAction<ITaskListStatusChangeResponse>) => {
|
updateEnhancedKanbanTaskStatus: (state, action: PayloadAction<ITaskListStatusChangeResponse>) => {
|
||||||
const { id: task_id, status_id } = action.payload;
|
const { id: task_id, status_id, color_code, color_code_dark, complete_ratio, statusCategory } = action.payload;
|
||||||
let oldGroupId: string | null = null;
|
let oldGroupId: string | null = null;
|
||||||
let foundTask: IProjectTask | null = null;
|
let foundTask: IProjectTask | null = null;
|
||||||
// Find the task and its group
|
// Find the task and its group
|
||||||
@@ -510,6 +528,14 @@ const enhancedKanbanSlice = createSlice({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!foundTask) return;
|
if (!foundTask) return;
|
||||||
|
|
||||||
|
// Update the task properties
|
||||||
|
foundTask.status_color = color_code;
|
||||||
|
foundTask.status_color_dark = color_code_dark;
|
||||||
|
foundTask.complete_ratio = +complete_ratio;
|
||||||
|
foundTask.status = status_id;
|
||||||
|
foundTask.status_category = statusCategory;
|
||||||
|
|
||||||
// If grouped by status and the group changes, move the task
|
// If grouped by status and the group changes, move the task
|
||||||
if (state.groupBy === IGroupBy.STATUS && oldGroupId && oldGroupId !== status_id) {
|
if (state.groupBy === IGroupBy.STATUS && oldGroupId && oldGroupId !== status_id) {
|
||||||
// Remove from old group
|
// Remove from old group
|
||||||
@@ -531,6 +557,128 @@ const enhancedKanbanSlice = createSlice({
|
|||||||
state.taskCache[task_id] = foundTask;
|
state.taskCache[task_id] = foundTask;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Enhanced Kanban priority update (for use in task drawer dropdown)
|
||||||
|
updateEnhancedKanbanTaskPriority: (state, action: PayloadAction<ITaskListPriorityChangeResponse>) => {
|
||||||
|
const { id, priority_id, color_code, color_code_dark } = action.payload;
|
||||||
|
|
||||||
|
// Find the task in any group
|
||||||
|
const taskInfo = findTaskInAllGroups(state.taskGroups, id);
|
||||||
|
if (!taskInfo || !priority_id) return;
|
||||||
|
|
||||||
|
const { task, groupId } = taskInfo;
|
||||||
|
|
||||||
|
// Update the task properties
|
||||||
|
task.priority = priority_id;
|
||||||
|
task.priority_color = color_code;
|
||||||
|
task.priority_color_dark = color_code_dark;
|
||||||
|
|
||||||
|
// If grouped by priority and not a subtask, move the task to the new priority group
|
||||||
|
if (
|
||||||
|
state.groupBy === IGroupBy.PRIORITY &&
|
||||||
|
!task.is_sub_task &&
|
||||||
|
groupId !== priority_id
|
||||||
|
) {
|
||||||
|
// Remove from current group
|
||||||
|
deleteTaskFromGroup(state.taskGroups, task, groupId);
|
||||||
|
|
||||||
|
// Add to new priority group
|
||||||
|
const newGroup = state.taskGroups.find(g => g.id === priority_id);
|
||||||
|
if (newGroup) {
|
||||||
|
newGroup.tasks.unshift(task);
|
||||||
|
state.groupCache[priority_id] = newGroup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
state.taskCache[id] = task;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Enhanced Kanban assignee update (for use in task drawer dropdown)
|
||||||
|
updateEnhancedKanbanTaskAssignees: (state, action: PayloadAction<ITaskAssigneesUpdateResponse>) => {
|
||||||
|
const { id, assignees, names } = action.payload;
|
||||||
|
|
||||||
|
// Find the task in any group
|
||||||
|
const taskInfo = findTaskInAllGroups(state.taskGroups, id);
|
||||||
|
if (!taskInfo) return;
|
||||||
|
|
||||||
|
const { task } = taskInfo;
|
||||||
|
|
||||||
|
// Update the task properties
|
||||||
|
task.assignees = assignees as ITaskAssignee[];
|
||||||
|
task.names = names as InlineMember[];
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
state.taskCache[id] = task;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Enhanced Kanban label update (for use in task drawer dropdown)
|
||||||
|
updateEnhancedKanbanTaskLabels: (state, action: PayloadAction<ILabelsChangeResponse>) => {
|
||||||
|
const label = action.payload;
|
||||||
|
for (const group of state.taskGroups) {
|
||||||
|
// Find the task or its subtask
|
||||||
|
const task =
|
||||||
|
group.tasks.find(task => task.id === label.id) ||
|
||||||
|
group.tasks
|
||||||
|
.flatMap(task => task.sub_tasks || [])
|
||||||
|
.find(subtask => subtask.id === label.id);
|
||||||
|
if (task) {
|
||||||
|
task.labels = label.labels || [];
|
||||||
|
task.all_labels = label.all_labels || [];
|
||||||
|
// Update cache
|
||||||
|
state.taskCache[label.id] = task;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Enhanced Kanban progress update (for use in task drawer and socket events)
|
||||||
|
updateEnhancedKanbanTaskProgress: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
id: string;
|
||||||
|
complete_ratio: number;
|
||||||
|
completed_count: number;
|
||||||
|
total_tasks_count: number;
|
||||||
|
parent_task: string;
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
const { id, complete_ratio, completed_count, total_tasks_count, parent_task } = action.payload;
|
||||||
|
|
||||||
|
// Find the task in any group
|
||||||
|
const taskInfo = findTaskInAllGroups(state.taskGroups, parent_task || id);
|
||||||
|
|
||||||
|
// Check if taskInfo exists before destructuring
|
||||||
|
if (!taskInfo) return;
|
||||||
|
|
||||||
|
const { task } = taskInfo;
|
||||||
|
|
||||||
|
// Update the task properties
|
||||||
|
task.complete_ratio = +complete_ratio;
|
||||||
|
task.completed_count = completed_count;
|
||||||
|
task.total_tasks_count = total_tasks_count;
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
state.taskCache[parent_task || id] = task;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Enhanced Kanban task name update (for use in task drawer header)
|
||||||
|
updateEnhancedKanbanTaskName: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
task: IProjectTask;
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
const { task } = action.payload;
|
||||||
|
|
||||||
|
// Find the task and update it
|
||||||
|
const result = findTaskInAllGroups(state.taskGroups, task.id || '');
|
||||||
|
if (result) {
|
||||||
|
result.task.name = task.name;
|
||||||
|
// Update cache
|
||||||
|
state.taskCache[task.id!] = result.task;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
updateTaskPriority: (state, action: PayloadAction<ITaskListPriorityChangeResponse>) => {
|
updateTaskPriority: (state, action: PayloadAction<ITaskListPriorityChangeResponse>) => {
|
||||||
const { id: task_id, priority_id } = action.payload;
|
const { id: task_id, priority_id } = action.payload;
|
||||||
|
|
||||||
@@ -608,6 +756,119 @@ const enhancedKanbanSlice = createSlice({
|
|||||||
const group = state.taskGroups.find(g => g.id === sectionId);
|
const group = state.taskGroups.find(g => g.id === sectionId);
|
||||||
if (group) {
|
if (group) {
|
||||||
group.tasks.push(task);
|
group.tasks.push(task);
|
||||||
|
// Update cache
|
||||||
|
state.taskCache[task.id!] = task;
|
||||||
|
state.groupCache[sectionId] = group;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Enhanced Kanban end date update (for use in task drawer and socket events)
|
||||||
|
updateEnhancedKanbanTaskEndDate: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
task: IProjectTask;
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
const { task } = action.payload;
|
||||||
|
|
||||||
|
// Find the task and update it
|
||||||
|
const result = findTaskInAllGroups(state.taskGroups, task.id || '');
|
||||||
|
if (result) {
|
||||||
|
result.task.end_date = task.end_date;
|
||||||
|
// Update cache
|
||||||
|
state.taskCache[task.id!] = result.task;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Enhanced Kanban start date update (for use in task drawer and socket events)
|
||||||
|
updateEnhancedKanbanTaskStartDate: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
task: IProjectTask;
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
const { task } = action.payload;
|
||||||
|
|
||||||
|
// Find the task and update it
|
||||||
|
const result = findTaskInAllGroups(state.taskGroups, task.id || '');
|
||||||
|
if (result) {
|
||||||
|
result.task.start_date = task.start_date;
|
||||||
|
// Update cache
|
||||||
|
state.taskCache[task.id!] = result.task;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Enhanced Kanban task expansion toggle (for subtask expand/collapse)
|
||||||
|
toggleTaskExpansion: (state, action: PayloadAction<string>) => {
|
||||||
|
const taskId = action.payload;
|
||||||
|
const result = findTaskInAllGroups(state.taskGroups, taskId);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
result.task.show_sub_tasks = !result.task.show_sub_tasks;
|
||||||
|
// Update cache
|
||||||
|
state.taskCache[taskId] = result.task;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Enhanced Kanban subtask update (for use in task drawer and socket events)
|
||||||
|
updateEnhancedKanbanSubtask: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
sectionId: string;
|
||||||
|
subtask: IProjectTask;
|
||||||
|
mode: 'add' | 'delete';
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
const { sectionId, subtask, mode } = action.payload;
|
||||||
|
const parentTaskId = subtask?.parent_task_id || null;
|
||||||
|
|
||||||
|
if (!parentTaskId) return;
|
||||||
|
|
||||||
|
// Function to update a task with a new subtask
|
||||||
|
const updateTaskWithSubtask = (task: IProjectTask): boolean => {
|
||||||
|
if (!task) return false;
|
||||||
|
|
||||||
|
// Initialize sub_tasks array if it doesn't exist
|
||||||
|
if (!task.sub_tasks) {
|
||||||
|
task.sub_tasks = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'add') {
|
||||||
|
// Increment subtask count
|
||||||
|
task.sub_tasks_count = (task.sub_tasks_count || 0) + 1;
|
||||||
|
|
||||||
|
// Add the subtask
|
||||||
|
task.sub_tasks.push({ ...subtask });
|
||||||
|
} else {
|
||||||
|
// Remove the subtask
|
||||||
|
task.sub_tasks = task.sub_tasks.filter(t => t.id !== subtask.id);
|
||||||
|
task.sub_tasks_count = Math.max(0, (task.sub_tasks_count || 1) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
state.taskCache[task.id!] = task;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// First try to find the task in the specified section
|
||||||
|
if (sectionId) {
|
||||||
|
const section = state.taskGroups.find(sec => sec.id === sectionId);
|
||||||
|
if (section) {
|
||||||
|
const task = section.tasks.find(task => task.id === parentTaskId);
|
||||||
|
if (task && updateTaskWithSubtask(task)) {
|
||||||
|
// Update group cache
|
||||||
|
state.groupCache[sectionId] = section;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found in the specified section, try all groups
|
||||||
|
const result = findTaskInAllGroups(state.taskGroups, parentTaskId);
|
||||||
|
if (result) {
|
||||||
|
updateTaskWithSubtask(result.task);
|
||||||
|
// Update group cache
|
||||||
|
state.groupCache[result.groupId] = result.group;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -624,10 +885,24 @@ const enhancedKanbanSlice = createSlice({
|
|||||||
// Update performance metrics
|
// Update performance metrics
|
||||||
state.performanceMetrics = calculatePerformanceMetrics(action.payload);
|
state.performanceMetrics = calculatePerformanceMetrics(action.payload);
|
||||||
|
|
||||||
// Update caches
|
// Update caches and initialize subtask properties
|
||||||
action.payload.forEach(group => {
|
action.payload.forEach(group => {
|
||||||
state.groupCache[group.id] = group;
|
state.groupCache[group.id] = group;
|
||||||
group.tasks.forEach(task => {
|
group.tasks.forEach(task => {
|
||||||
|
// Initialize subtask-related properties if they don't exist
|
||||||
|
if (task.sub_tasks === undefined) {
|
||||||
|
task.sub_tasks = [];
|
||||||
|
}
|
||||||
|
if (task.sub_tasks_loading === undefined) {
|
||||||
|
task.sub_tasks_loading = false;
|
||||||
|
}
|
||||||
|
if (task.show_sub_tasks === undefined) {
|
||||||
|
task.show_sub_tasks = false;
|
||||||
|
}
|
||||||
|
if (task.sub_tasks_count === undefined) {
|
||||||
|
task.sub_tasks_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
state.taskCache[task.id!] = task;
|
state.taskCache[task.id!] = task;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -702,6 +977,46 @@ const enhancedKanbanSlice = createSlice({
|
|||||||
.addCase(fetchEnhancedKanbanLabels.rejected, (state, action) => {
|
.addCase(fetchEnhancedKanbanLabels.rejected, (state, action) => {
|
||||||
state.loadingLabels = false;
|
state.loadingLabels = false;
|
||||||
state.error = action.payload as string;
|
state.error = action.payload as string;
|
||||||
|
})
|
||||||
|
// Fetch Board Sub Tasks
|
||||||
|
.addCase(fetchBoardSubTasks.pending, (state, action) => {
|
||||||
|
state.error = null;
|
||||||
|
// Find the task and set sub_tasks_loading to true
|
||||||
|
const taskId = action.meta.arg.taskId;
|
||||||
|
const result = findTaskInAllGroups(state.taskGroups, taskId);
|
||||||
|
if (result) {
|
||||||
|
result.task.sub_tasks_loading = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addCase(fetchBoardSubTasks.fulfilled, (state, action: PayloadAction<IProjectTask[]>) => {
|
||||||
|
const taskId = (action as any).meta?.arg?.taskId;
|
||||||
|
const result = findTaskInAllGroups(state.taskGroups, taskId);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
result.task.sub_tasks_loading = false;
|
||||||
|
result.task.sub_tasks = action.payload;
|
||||||
|
result.task.show_sub_tasks = true;
|
||||||
|
|
||||||
|
// Only update the count if we don't have a count yet or if the API returned a different count
|
||||||
|
// This preserves the original count from the initial data load
|
||||||
|
if (!result.task.sub_tasks_count || result.task.sub_tasks_count === 0) {
|
||||||
|
result.task.sub_tasks_count = action.payload.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
state.taskCache[taskId] = result.task;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.addCase(fetchBoardSubTasks.rejected, (state, action) => {
|
||||||
|
state.error = action.error.message || 'Failed to fetch sub tasks';
|
||||||
|
// Set loading to false on rejection
|
||||||
|
const taskId = action.meta.arg.taskId;
|
||||||
|
const result = findTaskInAllGroups(state.taskGroups, taskId);
|
||||||
|
if (result) {
|
||||||
|
result.task.sub_tasks_loading = false;
|
||||||
|
// Update cache
|
||||||
|
state.taskCache[taskId] = result.task;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -737,6 +1052,15 @@ export const {
|
|||||||
reorderGroups,
|
reorderGroups,
|
||||||
addTaskToGroup,
|
addTaskToGroup,
|
||||||
updateEnhancedKanbanTaskStatus,
|
updateEnhancedKanbanTaskStatus,
|
||||||
|
updateEnhancedKanbanTaskPriority,
|
||||||
|
updateEnhancedKanbanTaskAssignees,
|
||||||
|
updateEnhancedKanbanTaskLabels,
|
||||||
|
updateEnhancedKanbanTaskProgress,
|
||||||
|
updateEnhancedKanbanTaskName,
|
||||||
|
updateEnhancedKanbanTaskEndDate,
|
||||||
|
updateEnhancedKanbanTaskStartDate,
|
||||||
|
updateEnhancedKanbanSubtask,
|
||||||
|
toggleTaskExpansion,
|
||||||
} = enhancedKanbanSlice.actions;
|
} = enhancedKanbanSlice.actions;
|
||||||
|
|
||||||
export default enhancedKanbanSlice.reducer;
|
export default enhancedKanbanSlice.reducer;
|
||||||
@@ -42,6 +42,18 @@ import {
|
|||||||
selectCurrentGroupingV3,
|
selectCurrentGroupingV3,
|
||||||
fetchTasksV3
|
fetchTasksV3
|
||||||
} from '@/features/task-management/task-management.slice';
|
} from '@/features/task-management/task-management.slice';
|
||||||
|
import {
|
||||||
|
updateEnhancedKanbanSubtask,
|
||||||
|
addTaskToGroup as addEnhancedKanbanTaskToGroup,
|
||||||
|
updateEnhancedKanbanTaskStatus,
|
||||||
|
updateEnhancedKanbanTaskPriority,
|
||||||
|
updateEnhancedKanbanTaskAssignees,
|
||||||
|
updateEnhancedKanbanTaskLabels,
|
||||||
|
updateEnhancedKanbanTaskProgress,
|
||||||
|
updateEnhancedKanbanTaskName,
|
||||||
|
updateEnhancedKanbanTaskEndDate,
|
||||||
|
updateEnhancedKanbanTaskStartDate
|
||||||
|
} from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
import { selectCurrentGrouping } from '@/features/task-management/grouping.slice';
|
import { selectCurrentGrouping } from '@/features/task-management/grouping.slice';
|
||||||
import { fetchLabels } from '@/features/taskAttributes/taskLabelSlice';
|
import { fetchLabels } from '@/features/taskAttributes/taskLabelSlice';
|
||||||
import {
|
import {
|
||||||
@@ -111,6 +123,9 @@ export const useTaskSocketHandlers = () => {
|
|||||||
} as IProjectTask)
|
} as IProjectTask)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update enhanced kanban slice
|
||||||
|
dispatch(updateEnhancedKanbanTaskAssignees(data));
|
||||||
|
|
||||||
// Remove unnecessary refetch - real-time updates handle this
|
// Remove unnecessary refetch - real-time updates handle this
|
||||||
// if (currentSession?.team_id && !loadingAssignees) {
|
// if (currentSession?.team_id && !loadingAssignees) {
|
||||||
// dispatch(fetchTaskAssignees(currentSession.team_id));
|
// dispatch(fetchTaskAssignees(currentSession.team_id));
|
||||||
@@ -150,6 +165,9 @@ export const useTaskSocketHandlers = () => {
|
|||||||
// dispatch(fetchLabels()),
|
// dispatch(fetchLabels()),
|
||||||
// projectId && dispatch(fetchLabelsByProject(projectId)),
|
// projectId && dispatch(fetchLabelsByProject(projectId)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Update enhanced kanban slice
|
||||||
|
dispatch(updateEnhancedKanbanTaskLabels(labels));
|
||||||
},
|
},
|
||||||
[dispatch, projectId]
|
[dispatch, projectId]
|
||||||
);
|
);
|
||||||
@@ -172,6 +190,9 @@ export const useTaskSocketHandlers = () => {
|
|||||||
dispatch(updateTaskStatus(response));
|
dispatch(updateTaskStatus(response));
|
||||||
dispatch(deselectAll());
|
dispatch(deselectAll());
|
||||||
|
|
||||||
|
// Update enhanced kanban slice
|
||||||
|
dispatch(updateEnhancedKanbanTaskStatus(response));
|
||||||
|
|
||||||
// For the task management slice, move task between groups without resetting
|
// For the task management slice, move task between groups without resetting
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
const groups = state.taskManagement.groups;
|
const groups = state.taskManagement.groups;
|
||||||
@@ -267,6 +288,15 @@ export const useTaskSocketHandlers = () => {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update enhanced kanban slice
|
||||||
|
dispatch(updateEnhancedKanbanTaskProgress({
|
||||||
|
id: data.id,
|
||||||
|
complete_ratio: data.complete_ratio,
|
||||||
|
completed_count: data.completed_count,
|
||||||
|
total_tasks_count: data.total_tasks_count,
|
||||||
|
parent_task: data.parent_task,
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
@@ -282,6 +312,9 @@ export const useTaskSocketHandlers = () => {
|
|||||||
dispatch(setTaskPriority(response));
|
dispatch(setTaskPriority(response));
|
||||||
dispatch(deselectAll());
|
dispatch(deselectAll());
|
||||||
|
|
||||||
|
// Update enhanced kanban slice
|
||||||
|
dispatch(updateEnhancedKanbanTaskPriority(response));
|
||||||
|
|
||||||
// For the task management slice, always update the task entity first
|
// For the task management slice, always update the task entity first
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
const currentTask = state.taskManagement.entities[response.id];
|
const currentTask = state.taskManagement.entities[response.id];
|
||||||
@@ -371,6 +404,9 @@ export const useTaskSocketHandlers = () => {
|
|||||||
|
|
||||||
dispatch(updateTaskEndDate({ task: taskWithProgress }));
|
dispatch(updateTaskEndDate({ task: taskWithProgress }));
|
||||||
dispatch(setTaskEndDate(taskWithProgress));
|
dispatch(setTaskEndDate(taskWithProgress));
|
||||||
|
|
||||||
|
// Update enhanced kanban slice
|
||||||
|
dispatch(updateEnhancedKanbanTaskEndDate({ task: taskWithProgress }));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
@@ -392,6 +428,9 @@ export const useTaskSocketHandlers = () => {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update enhanced kanban slice
|
||||||
|
dispatch(updateEnhancedKanbanTaskName({ task: data }));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
@@ -558,6 +597,13 @@ export const useTaskSocketHandlers = () => {
|
|||||||
if (data.parent_task_id) {
|
if (data.parent_task_id) {
|
||||||
// Handle subtask creation
|
// Handle subtask creation
|
||||||
dispatch(updateSubTasks(data));
|
dispatch(updateSubTasks(data));
|
||||||
|
|
||||||
|
// Also update enhanced kanban slice for subtask creation
|
||||||
|
dispatch(updateEnhancedKanbanSubtask({
|
||||||
|
sectionId: '',
|
||||||
|
subtask: data,
|
||||||
|
mode: 'add'
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
// Handle regular task creation - transform to Task format and add
|
// Handle regular task creation - transform to Task format and add
|
||||||
const task = {
|
const task = {
|
||||||
@@ -613,6 +659,12 @@ export const useTaskSocketHandlers = () => {
|
|||||||
|
|
||||||
// Use addTaskToGroup with the actual group UUID
|
// Use addTaskToGroup with the actual group UUID
|
||||||
dispatch(addTaskToGroup({ task, groupId }));
|
dispatch(addTaskToGroup({ task, groupId }));
|
||||||
|
|
||||||
|
// Also update enhanced kanban slice for regular task creation
|
||||||
|
dispatch(addEnhancedKanbanTaskToGroup({
|
||||||
|
sectionId: groupId || '',
|
||||||
|
task: data
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
|
|||||||
@@ -2,20 +2,6 @@ import React from 'react';
|
|||||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
import TaskListBoard from '@/components/task-management/task-list-board';
|
import TaskListBoard from '@/components/task-management/task-list-board';
|
||||||
|
|
||||||
/**
|
|
||||||
* Enhanced Tasks View - Optimized for Performance
|
|
||||||
*
|
|
||||||
* PERFORMANCE IMPROVEMENTS:
|
|
||||||
* - Task loading is now ~5x faster (200-500ms vs 2-5s previously)
|
|
||||||
* - Progress calculations are skipped by default to improve initial load
|
|
||||||
* - Real-time updates still work via socket connections
|
|
||||||
* - Performance monitoring available in development mode
|
|
||||||
*
|
|
||||||
* If you're experiencing slow loading:
|
|
||||||
* 1. Check the browser console for performance metrics
|
|
||||||
* 2. Performance alerts will show automatically if loading > 2 seconds
|
|
||||||
* 3. Contact support if issues persist
|
|
||||||
*/
|
|
||||||
const ProjectViewEnhancedTasks: React.FC = () => {
|
const ProjectViewEnhancedTasks: React.FC = () => {
|
||||||
const { project } = useAppSelector(state => state.projectReducer);
|
const { project } = useAppSelector(state => state.projectReducer);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user