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:
chamikaJ
2025-07-03 15:31:54 +05:30
parent e26f16bbc2
commit d15c00c29b
19 changed files with 1316 additions and 602 deletions

View File

@@ -0,0 +1,58 @@
import React from 'react';
import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
interface TaskGroupHeaderProps {
group: {
id: string;
name: string;
count: number;
color?: string; // Color for the group indicator
};
isCollapsed: boolean;
onToggle: () => void;
}
const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, onToggle }) => {
return (
<div
className="flex items-center px-4 py-2 bg-white dark:bg-gray-800 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 border-b border-gray-200 dark:border-gray-700"
onClick={onToggle}
>
{/* Chevron button */}
<button
className="p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
onClick={(e) => {
e.stopPropagation();
onToggle();
}}
>
{isCollapsed ? (
<ChevronRightIcon className="h-4 w-4 text-gray-400 dark:text-gray-500" />
) : (
<ChevronDownIcon className="h-4 w-4 text-gray-400 dark:text-gray-500" />
)}
</button>
{/* Group indicator and name */}
<div className="ml-2 flex items-center gap-3 flex-1">
{/* Color indicator */}
<div
className="w-3 h-3 rounded-sm"
style={{ backgroundColor: group.color || '#94A3B8' }}
/>
{/* Group name and count */}
<div className="flex items-center justify-between flex-1">
<span className="text-sm font-medium text-gray-900 dark:text-white">
{group.name}
</span>
<span className="text-xs font-medium text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded-full">
{group.count}
</span>
</div>
</div>
</div>
);
};
export default TaskGroupHeader;

View File

@@ -0,0 +1,264 @@
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import { GroupedVirtuoso } from 'react-virtuoso';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import {
selectAllTasksArray,
selectGroups,
selectGrouping,
selectLoading,
selectError,
selectSelectedPriorities,
selectSearch,
fetchTasksV3,
} from '@/features/task-management/task-management.slice';
import {
selectCurrentGrouping,
selectCollapsedGroups,
selectIsGroupCollapsed,
toggleGroupCollapsed,
} from '@/features/task-management/grouping.slice';
import {
selectSelectedTaskIds,
selectLastSelectedTaskId,
selectIsTaskSelected,
selectTask,
deselectTask,
toggleTaskSelection,
selectRange,
clearSelection,
} from '@/features/task-management/selection.slice';
import TaskRow from './TaskRow';
import TaskGroupHeader from './TaskGroupHeader';
import { Task, TaskGroup } from '@/types/task-management.types';
import { RootState } from '@/app/store';
import { TaskListField } from '@/types/task-list-field.types';
import { useParams } from 'react-router-dom';
import ImprovedTaskFilters from '@/components/task-management/improved-task-filters';
import { Bars3Icon } from '@heroicons/react/24/outline';
import { COLUMN_KEYS } from '@/features/tasks/tasks.slice';
// Base column configuration
const BASE_COLUMNS = [
{ id: 'dragHandle', label: '', width: '32px', isSticky: true, key: 'dragHandle' },
{ id: 'taskKey', label: 'Key', width: '100px', isSticky: true, key: COLUMN_KEYS.KEY },
{ id: 'title', label: 'Title', width: '300px', isSticky: true, key: COLUMN_KEYS.NAME },
{ id: 'status', label: 'Status', width: '120px', key: COLUMN_KEYS.STATUS },
{ id: 'assignees', label: 'Assignees', width: '150px', key: COLUMN_KEYS.ASSIGNEES },
{ id: 'priority', label: 'Priority', width: '120px', key: COLUMN_KEYS.PRIORITY },
{ id: 'dueDate', label: 'Due Date', width: '120px', key: COLUMN_KEYS.DUE_DATE },
{ id: 'progress', label: 'Progress', width: '120px', key: COLUMN_KEYS.PROGRESS },
{ id: 'labels', label: 'Labels', width: '150px', key: COLUMN_KEYS.LABELS },
{ id: 'phase', label: 'Phase', width: '120px', key: COLUMN_KEYS.PHASE },
{ id: 'timeTracking', label: 'Time Tracking', width: '120px', key: COLUMN_KEYS.TIME_TRACKING },
{ id: 'estimation', label: 'Estimation', width: '120px', key: COLUMN_KEYS.ESTIMATION },
{ id: 'startDate', label: 'Start Date', width: '120px', key: COLUMN_KEYS.START_DATE },
{ id: 'dueTime', label: 'Due Time', width: '120px', key: COLUMN_KEYS.DUE_TIME },
{ id: 'completedDate', label: 'Completed Date', width: '120px', key: COLUMN_KEYS.COMPLETED_DATE },
{ id: 'createdDate', label: 'Created Date', width: '120px', key: COLUMN_KEYS.CREATED_DATE },
{ id: 'lastUpdated', label: 'Last Updated', width: '120px', key: COLUMN_KEYS.LAST_UPDATED },
{ id: 'reporter', label: 'Reporter', width: '120px', key: COLUMN_KEYS.REPORTER },
];
type ColumnStyle = {
width: string;
position?: 'static' | 'relative' | 'absolute' | 'sticky' | 'fixed';
left?: number;
backgroundColor?: string;
zIndex?: number;
};
interface TaskListV2Props {
projectId: string;
}
const TaskListV2: React.FC<TaskListV2Props> = ({ projectId }) => {
const dispatch = useAppDispatch();
const { projectId: urlProjectId } = useParams();
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set());
// Selectors
const tasks = useAppSelector(selectAllTasksArray);
const groups = useAppSelector(selectGroups);
const grouping = useAppSelector(selectGrouping);
const loading = useAppSelector(selectLoading);
const error = useAppSelector(selectError);
const selectedPriorities = useAppSelector(selectSelectedPriorities);
const searchQuery = useAppSelector(selectSearch);
const currentGrouping = useAppSelector(selectCurrentGrouping);
const selectedTaskIds = useAppSelector(selectSelectedTaskIds);
const lastSelectedTaskId = useAppSelector(selectLastSelectedTaskId);
const fields = useAppSelector(state => state.taskManagementFields) || [];
// Filter visible columns based on fields
const visibleColumns = useMemo(() => {
return BASE_COLUMNS.filter(column => {
// Always show drag handle, task key, and title
if (column.isSticky) return true;
// Check if field is visible
const field = fields.find(f => f.key === column.key);
return field?.visible ?? false;
});
}, [fields]);
// Effects
useEffect(() => {
if (urlProjectId) {
dispatch(fetchTasksV3(urlProjectId));
}
}, [dispatch, urlProjectId]);
// Handlers
const handleTaskSelect = useCallback((taskId: string, event: React.MouseEvent) => {
if (event.ctrlKey || event.metaKey) {
dispatch(toggleTaskSelection(taskId));
} else if (event.shiftKey && lastSelectedTaskId) {
const taskIds = tasks.map(t => t.id);
const startIdx = taskIds.indexOf(lastSelectedTaskId);
const endIdx = taskIds.indexOf(taskId);
const rangeIds = taskIds.slice(
Math.min(startIdx, endIdx),
Math.max(startIdx, endIdx) + 1
);
dispatch(selectRange(rangeIds));
} else {
dispatch(clearSelection());
dispatch(selectTask(taskId));
}
}, [dispatch, lastSelectedTaskId, tasks]);
const handleGroupCollapse = useCallback((groupId: string) => {
setCollapsedGroups(prev => {
const next = new Set(prev);
if (next.has(groupId)) {
next.delete(groupId);
} else {
next.add(groupId);
}
return next;
});
}, []);
// Memoized values
const groupCounts = useMemo(() => {
return groups.map(group => {
const visibleTasks = tasks.filter(task => group.taskIds.includes(task.id));
return visibleTasks.length;
});
}, [groups, tasks]);
const visibleGroups = useMemo(() => {
return groups.filter(group => !collapsedGroups.has(group.id));
}, [groups, collapsedGroups]);
// Render functions
const renderGroup = useCallback((groupIndex: number) => {
const group = groups[groupIndex];
return (
<TaskGroupHeader
group={{
id: group.id,
name: group.title,
count: groupCounts[groupIndex],
color: group.color,
}}
isCollapsed={collapsedGroups.has(group.id)}
onToggle={() => handleGroupCollapse(group.id)}
/>
);
}, [groups, groupCounts, collapsedGroups, handleGroupCollapse]);
const renderTask = useCallback((taskIndex: number) => {
const task = tasks[taskIndex];
return (
<TaskRow
task={task}
visibleColumns={visibleColumns}
/>
);
}, [tasks, visibleColumns]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
// Log data for debugging
console.log('Rendering with:', {
groups,
tasks,
groupCounts
});
return (
<div className="flex flex-col h-screen bg-white dark:bg-gray-900">
{/* Task Filters */}
<div className="flex-none px-4 py-3">
<ImprovedTaskFilters position="list" />
</div>
{/* Column Headers */}
<div className="overflow-x-auto">
<div className="flex-none border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
<div className="flex items-center min-w-max px-4 py-2">
{visibleColumns.map((column, index) => {
const columnStyle: ColumnStyle = {
width: column.width,
...(column.isSticky ? {
position: 'sticky',
left: index === 0 ? 0 : index === 1 ? 32 : 132,
backgroundColor: 'inherit',
zIndex: 2,
} : {}),
};
return (
<div
key={column.id}
className="text-xs font-medium text-gray-500 dark:text-gray-400"
style={columnStyle}
>
{column.id === 'dragHandle' ? (
<Bars3Icon className="w-4 h-4 text-gray-400" />
) : (
column.label
)}
</div>
);
})}
</div>
</div>
{/* Task List */}
<div className="flex-1 overflow-hidden">
<GroupedVirtuoso
style={{ height: 'calc(100vh - 200px)' }}
groupCounts={groupCounts}
groupContent={renderGroup}
itemContent={renderTask}
components={{
Group: ({ children, ...props }) => (
<div
{...props}
className="sticky top-0 z-10 bg-white dark:bg-gray-800"
>
{children}
</div>
),
List: React.forwardRef(({ style, children }, ref) => (
<div
ref={ref as any}
style={style}
className="divide-y divide-gray-200 dark:divide-gray-700"
>
{children}
</div>
)),
}}
/>
</div>
</div>
</div>
);
};
export default TaskListV2;

View File

@@ -0,0 +1,261 @@
import React from 'react';
import { Task } from '@/types/task-management.types';
import Avatar from '@/components/Avatar';
import { format } from 'date-fns';
import { Bars3Icon } from '@heroicons/react/24/outline';
import { ClockIcon } from '@heroicons/react/24/outline';
interface TaskRowProps {
task: Task;
visibleColumns: Array<{
id: string;
width: string;
isSticky?: boolean;
}>;
}
const TaskRow: React.FC<TaskRowProps> = ({ task, visibleColumns }) => {
const renderColumn = (columnId: string, width: string, isSticky?: boolean, index?: number) => {
const baseStyle = {
width,
...(isSticky ? {
position: 'sticky' as const,
left: index === 0 ? 0 : index === 1 ? 32 : 132,
backgroundColor: 'inherit',
zIndex: 1,
} : {}),
};
switch (columnId) {
case 'dragHandle':
return (
<div
className="cursor-move flex items-center justify-center"
style={baseStyle}
>
<Bars3Icon className="w-4 h-4 text-gray-400" />
</div>
);
case 'taskKey':
return (
<div
className="flex items-center"
style={baseStyle}
>
<span className="text-sm font-medium text-gray-900 dark:text-white whitespace-nowrap">
{task.task_key}
</span>
</div>
);
case 'title':
return (
<div
className="flex items-center"
style={baseStyle}
>
<span className="text-sm text-gray-700 dark:text-gray-300 truncate">
{task.title || task.name}
</span>
</div>
);
case 'status':
return (
<div style={baseStyle}>
<span
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
style={{
backgroundColor: task.statusColor ? `${task.statusColor}20` : 'rgb(229, 231, 235)',
color: task.statusColor || 'rgb(31, 41, 55)',
}}
>
{task.status}
</span>
</div>
);
case 'assignees':
return (
<div className="flex items-center gap-1" style={baseStyle}>
{task.assignee_names?.slice(0, 3).map((assignee, index) => (
<Avatar
key={index}
name={assignee.name || ''}
size="small"
className="ring-2 ring-white dark:ring-gray-900"
/>
))}
{(task.assignee_names?.length || 0) > 3 && (
<span className="text-xs text-gray-500 dark:text-gray-400 ml-1">
+{task.assignee_names!.length - 3}
</span>
)}
</div>
);
case 'priority':
return (
<div style={baseStyle}>
<span
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
style={{
backgroundColor: task.priorityColor ? `${task.priorityColor}20` : 'rgb(229, 231, 235)',
color: task.priorityColor || 'rgb(31, 41, 55)',
}}
>
{task.priority}
</span>
</div>
);
case 'dueDate':
return (
<div style={baseStyle}>
{task.dueDate && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{format(new Date(task.dueDate), 'MMM d')}
</span>
)}
</div>
);
case 'progress':
return (
<div style={baseStyle}>
<div className="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${task.progress}%` }}
/>
</div>
</div>
);
case 'labels':
return (
<div className="flex items-center gap-1" style={baseStyle}>
{task.labels?.slice(0, 2).map((label, index) => (
<span
key={index}
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
style={{
backgroundColor: label.color ? `${label.color}20` : 'rgb(229, 231, 235)',
color: label.color || 'rgb(31, 41, 55)',
}}
>
{label.name}
</span>
))}
{(task.labels?.length || 0) > 2 && (
<span className="text-xs text-gray-500 dark:text-gray-400">
+{task.labels!.length - 2}
</span>
)}
</div>
);
case 'phase':
return (
<div style={baseStyle}>
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200">
{task.phase}
</span>
</div>
);
case 'timeTracking':
return (
<div className="flex items-center gap-1" style={baseStyle}>
<ClockIcon className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-500 dark:text-gray-400">
{task.timeTracking?.logged || 0}h
</span>
{task.timeTracking?.estimated && (
<span className="text-sm text-gray-400 dark:text-gray-500">
/{task.timeTracking.estimated}h
</span>
)}
</div>
);
case 'estimation':
return (
<div style={baseStyle}>
{task.timeTracking.estimated && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{task.timeTracking.estimated}h
</span>
)}
</div>
);
case 'startDate':
return (
<div style={baseStyle}>
{task.startDate && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{format(new Date(task.startDate), 'MMM d')}
</span>
)}
</div>
);
case 'completedDate':
return (
<div style={baseStyle}>
{task.completedAt && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{format(new Date(task.completedAt), 'MMM d')}
</span>
)}
</div>
);
case 'createdDate':
return (
<div style={baseStyle}>
{task.createdAt && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{format(new Date(task.createdAt), 'MMM d')}
</span>
)}
</div>
);
case 'lastUpdated':
return (
<div style={baseStyle}>
{task.updatedAt && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{format(new Date(task.updatedAt), 'MMM d')}
</span>
)}
</div>
);
case 'reporter':
return (
<div style={baseStyle}>
{task.reporter && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{task.reporter}
</span>
)}
</div>
);
default:
return null;
}
};
return (
<div className="flex items-center min-w-max px-4 py-2 hover:bg-gray-50 dark:hover:bg-gray-800">
{visibleColumns.map((column, index) => renderColumn(column.id, column.width, column.isSticky, index))}
</div>
);
};
export default TaskRow;

View File

@@ -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>

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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';