feat(task-management): enhance task list with new components and improved state management
- Introduced TaskListV2 and TaskGroupHeader components for a more organized task display. - Implemented virtualized rendering using react-virtuoso for efficient task list handling. - Updated Redux state management to include new selectors and improved task grouping logic. - Added task filtering capabilities with TaskListFilters component for better user experience. - Enhanced task selection handling and integrated drag-and-drop functionality for task rows. - Updated package dependencies to include new libraries for icons and forms.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
@@ -11,12 +11,15 @@ import {
|
||||
DownOutlined,
|
||||
} from '@/shared/antd-imports';
|
||||
import { TaskGroup as TaskGroupType, Task } from '@/types/task-management.types';
|
||||
import { taskManagementSelectors } from '@/features/task-management/task-management.slice';
|
||||
import { taskManagementSelectors, selectAllTasks } from '@/features/task-management/task-management.slice';
|
||||
import { RootState } from '@/app/store';
|
||||
import TaskRow from './task-row';
|
||||
import AddTaskListRow from '@/pages/projects/projectView/taskList/task-list-table/task-list-table-rows/add-task-list-row';
|
||||
import { TaskListField } from '@/features/task-management/taskListFields.slice';
|
||||
import { Checkbox } from '@/components';
|
||||
import { selectIsGroupCollapsed, toggleGroupCollapsed } from '@/features/task-management/grouping.slice';
|
||||
import { selectIsTaskSelected } from '@/features/task-management/selection.slice';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -58,6 +61,7 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(
|
||||
onSelectTask,
|
||||
onToggleSubtasks,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const [isCollapsed, setIsCollapsed] = useState(group.collapsed || false);
|
||||
|
||||
const { setNodeRef, isOver } = useDroppable({
|
||||
@@ -69,7 +73,7 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(
|
||||
});
|
||||
|
||||
// Get all tasks from the store
|
||||
const allTasks = useSelector(taskManagementSelectors.selectAll);
|
||||
const allTasks = useSelector(selectAllTasks);
|
||||
|
||||
// Get theme from Redux store
|
||||
const isDarkMode = useSelector((state: RootState) => state.themeReducer?.mode === 'dark');
|
||||
@@ -328,19 +332,29 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(
|
||||
<SortableContext items={group.taskIds} strategy={verticalListSortingStrategy}>
|
||||
<div className="task-group-tasks">
|
||||
{groupTasks.map((task, index) => (
|
||||
<TaskRow
|
||||
key={task.id}
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
groupId={group.id}
|
||||
currentGrouping={currentGrouping}
|
||||
isSelected={selectedTaskIds.includes(task.id)}
|
||||
index={index}
|
||||
onSelect={onSelectTask}
|
||||
onToggleSubtasks={onToggleSubtasks}
|
||||
fixedColumns={visibleFixedColumns}
|
||||
scrollableColumns={visibleScrollableColumns}
|
||||
/>
|
||||
<Draggable key={task.id} draggableId={task.id} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
className={`task-row-wrapper ${snapshot.isDragging ? 'dragging' : ''}`}
|
||||
>
|
||||
<TaskRow
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
groupId={group.id}
|
||||
currentGrouping={currentGrouping}
|
||||
isSelected={selectedTaskIds.includes(task.id)}
|
||||
index={index}
|
||||
onSelect={onSelectTask}
|
||||
onToggleSubtasks={onToggleSubtasks}
|
||||
fixedColumns={visibleFixedColumns}
|
||||
scrollableColumns={visibleScrollableColumns}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
|
||||
@@ -17,35 +17,50 @@ import { sortableKeyboardCoordinates } from '@dnd-kit/sortable';
|
||||
import { Card, Spin, Empty, Alert } from 'antd';
|
||||
import { RootState } from '@/app/store';
|
||||
import {
|
||||
taskManagementSelectors,
|
||||
selectAllTasks,
|
||||
selectGroups,
|
||||
selectGrouping,
|
||||
selectLoading,
|
||||
selectError,
|
||||
selectSelectedPriorities,
|
||||
selectSearch,
|
||||
reorderTasks,
|
||||
moveTaskToGroup,
|
||||
moveTaskBetweenGroups,
|
||||
optimisticTaskMove,
|
||||
reorderTasksInGroup,
|
||||
setLoading,
|
||||
fetchTasks,
|
||||
fetchTasksV3,
|
||||
selectTaskGroupsV3,
|
||||
selectCurrentGroupingV3,
|
||||
fetchSubTasks,
|
||||
setError,
|
||||
setSelectedPriorities,
|
||||
setSearch,
|
||||
resetTaskManagement,
|
||||
toggleTaskExpansion,
|
||||
addSubtaskToParent,
|
||||
fetchTasksV3,
|
||||
} from '@/features/task-management/task-management.slice';
|
||||
import {
|
||||
selectTaskGroups,
|
||||
selectCurrentGrouping,
|
||||
setCurrentGrouping,
|
||||
selectCollapsedGroups,
|
||||
selectIsGroupCollapsed,
|
||||
toggleGroupCollapsed,
|
||||
expandAllGroups,
|
||||
collapseAllGroups,
|
||||
} from '@/features/task-management/grouping.slice';
|
||||
import {
|
||||
selectSelectedTaskIds,
|
||||
selectLastSelectedTaskId,
|
||||
selectIsTaskSelected,
|
||||
selectTask,
|
||||
deselectTask,
|
||||
toggleTaskSelection,
|
||||
selectRange,
|
||||
clearSelection,
|
||||
} from '@/features/task-management/selection.slice';
|
||||
import {
|
||||
selectTaskIds,
|
||||
selectTasks,
|
||||
deselectAll as deselectAllBulk,
|
||||
} from '@/features/projects/bulkActions/bulkActionSlice';
|
||||
import { Task } from '@/types/task-management.types';
|
||||
import { Task, TaskGroup } from '@/types/task-management.types';
|
||||
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
@@ -157,16 +172,19 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
|
||||
const { socket, connected } = useSocket();
|
||||
|
||||
// Redux selectors using V3 API (pre-processed data, minimal loops)
|
||||
const tasks = useSelector(taskManagementSelectors.selectAll);
|
||||
const tasks = useSelector(selectAllTasks);
|
||||
const groups = useSelector(selectGroups);
|
||||
const grouping = useSelector(selectGrouping);
|
||||
const loading = useSelector(selectLoading);
|
||||
const error = useSelector(selectError);
|
||||
const selectedPriorities = useSelector(selectSelectedPriorities);
|
||||
const searchQuery = useSelector(selectSearch);
|
||||
const taskGroups = useSelector(selectTaskGroupsV3, shallowEqual);
|
||||
const currentGrouping = useSelector(selectCurrentGroupingV3, shallowEqual);
|
||||
// Use bulk action slice for selected tasks instead of selection slice
|
||||
const selectedTaskIds = useSelector(
|
||||
(state: RootState) => state.bulkActionReducer.selectedTaskIdsList
|
||||
);
|
||||
const currentGrouping = useSelector(selectCurrentGrouping);
|
||||
const collapsedGroups = useSelector(selectCollapsedGroups);
|
||||
const selectedTaskIds = useSelector(selectSelectedTaskIds);
|
||||
const lastSelectedTaskId = useSelector(selectLastSelectedTaskId);
|
||||
const selectedTasks = useSelector((state: RootState) => state.bulkActionReducer.selectedTasks);
|
||||
const loading = useSelector((state: RootState) => state.taskManagement.loading, shallowEqual);
|
||||
const error = useSelector((state: RootState) => state.taskManagement.error);
|
||||
|
||||
// Bulk action selectors
|
||||
const statusList = useSelector((state: RootState) => state.taskStatusReducer.status);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
|
||||
interface TaskListFiltersProps {
|
||||
selectedPriorities: string[];
|
||||
onPriorityChange: (priorities: string[]) => void;
|
||||
searchQuery: string;
|
||||
onSearchChange: (query: string) => void;
|
||||
}
|
||||
|
||||
const TaskListFilters: React.FC<TaskListFiltersProps> = ({
|
||||
selectedPriorities,
|
||||
onPriorityChange,
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
}) => {
|
||||
const priorities = ['High', 'Medium', 'Low'];
|
||||
|
||||
return (
|
||||
<div className="task-list-filters">
|
||||
<div className="filter-group">
|
||||
<label>Priority:</label>
|
||||
<div className="priority-filters">
|
||||
{priorities.map(priority => (
|
||||
<label key={priority} className="priority-filter">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedPriorities.includes(priority)}
|
||||
onChange={e => {
|
||||
const newPriorities = e.target.checked
|
||||
? [...selectedPriorities, priority]
|
||||
: selectedPriorities.filter(p => p !== priority);
|
||||
onPriorityChange(newPriorities);
|
||||
}}
|
||||
/>
|
||||
<span>{priority}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<label>Search:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={e => onSearchChange(e.target.value)}
|
||||
placeholder="Search tasks..."
|
||||
className="search-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskListFilters;
|
||||
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Task, TaskGroup } from '@/types/task-management.types';
|
||||
import TaskRow from './task-row';
|
||||
|
||||
interface TaskListGroupProps {
|
||||
group: TaskGroup;
|
||||
tasks: Task[];
|
||||
isCollapsed: boolean;
|
||||
onCollapse: () => void;
|
||||
onTaskSelect: (taskId: string, event: React.MouseEvent) => void;
|
||||
selectedTaskIds: string[];
|
||||
projectId: string;
|
||||
currentGrouping: 'status' | 'priority' | 'phase';
|
||||
}
|
||||
|
||||
const TaskListGroup: React.FC<TaskListGroupProps> = ({
|
||||
group,
|
||||
tasks,
|
||||
isCollapsed,
|
||||
onCollapse,
|
||||
onTaskSelect,
|
||||
selectedTaskIds,
|
||||
projectId,
|
||||
currentGrouping,
|
||||
}) => {
|
||||
const groupStyle = {
|
||||
backgroundColor: group.color ? `${group.color}10` : undefined,
|
||||
borderColor: group.color,
|
||||
};
|
||||
|
||||
const headerStyle = {
|
||||
backgroundColor: group.color ? `${group.color}20` : undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="task-list-group" style={groupStyle}>
|
||||
<div className="group-header" style={headerStyle} onClick={onCollapse}>
|
||||
<div className="group-title">
|
||||
<span className={`collapse-icon ${isCollapsed ? 'collapsed' : ''}`}>
|
||||
{isCollapsed ? '►' : '▼'}
|
||||
</span>
|
||||
<h3>{group.title}</h3>
|
||||
<span className="task-count">({tasks.length})</span>
|
||||
</div>
|
||||
</div>
|
||||
{!isCollapsed && (
|
||||
<div className="task-list">
|
||||
{tasks.map((task, index) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id: task.id,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={task.id}
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className={`task-row-wrapper ${isDragging ? 'dragging' : ''}`}
|
||||
>
|
||||
<TaskRow
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
groupId={group.id}
|
||||
currentGrouping={currentGrouping}
|
||||
isSelected={selectedTaskIds.includes(task.id)}
|
||||
onSelect={(taskId, selected) => onTaskSelect(taskId, {} as React.MouseEvent)}
|
||||
index={index}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskListGroup;
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
|
||||
interface TaskListHeaderProps {
|
||||
onExpandAll: () => void;
|
||||
onCollapseAll: () => void;
|
||||
}
|
||||
|
||||
const TaskListHeader: React.FC<TaskListHeaderProps> = ({
|
||||
onExpandAll,
|
||||
onCollapseAll,
|
||||
}) => {
|
||||
return (
|
||||
<div className="task-list-header">
|
||||
<div className="header-actions">
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={onExpandAll}
|
||||
>
|
||||
Expand All
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm ml-2"
|
||||
onClick={onCollapseAll}
|
||||
>
|
||||
Collapse All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskListHeader;
|
||||
@@ -9,6 +9,14 @@ import {
|
||||
taskManagementSelectors,
|
||||
toggleTaskExpansion,
|
||||
fetchSubTasks,
|
||||
selectAllTasks,
|
||||
selectTaskIds,
|
||||
selectGroups,
|
||||
selectGrouping,
|
||||
selectLoading,
|
||||
selectError,
|
||||
selectSelectedPriorities,
|
||||
selectSearch,
|
||||
} from '@/features/task-management/task-management.slice';
|
||||
import { toggleGroupCollapsed } from '@/features/task-management/grouping.slice';
|
||||
import { Task } from '@/types/task-management.types';
|
||||
|
||||
Reference in New Issue
Block a user