Merge pull request #165 from Worklenz/imp/task-list-performance-fixes

Imp/task list performance fixes
This commit is contained in:
Chamika J
2025-06-20 08:40:14 +05:30
committed by GitHub
18 changed files with 4082 additions and 32 deletions

View File

@@ -0,0 +1,429 @@
# Enhanced Task Management: Technical Guide
## Overview
The Enhanced Task Management system is a comprehensive React-based interface built on top of WorkLenz's existing task infrastructure. It provides a modern, grouped view with drag-and-drop functionality, bulk operations, and responsive design.
## Architecture
### Component Structure
```
src/components/task-management/
├── TaskListBoard.tsx # Main container with DnD context
├── TaskGroup.tsx # Individual group with collapse/expand
├── TaskRow.tsx # Task display with rich metadata
├── GroupingSelector.tsx # Grouping method switcher
└── BulkActionBar.tsx # Bulk operations toolbar
```
### Integration Points
The system integrates with existing WorkLenz infrastructure:
- **Redux Store:** Uses `tasks.slice.ts` for state management
- **Types:** Leverages existing TypeScript interfaces
- **API Services:** Works with existing task API endpoints
- **WebSocket:** Supports real-time updates via existing socket system
## Core Components
### TaskListBoard.tsx
Main orchestrator component that provides:
- **DnD Context:** @dnd-kit drag-and-drop functionality
- **State Management:** Redux integration for task data
- **Event Handling:** Drag events and bulk operations
- **Layout Structure:** Header controls and group container
#### Key Props
```typescript
interface TaskListBoardProps {
projectId: string; // Required: Project identifier
className?: string; // Optional: Additional CSS classes
}
```
#### Redux Selectors Used
```typescript
const {
taskGroups, // ITaskListGroup[] - Grouped task data
loadingGroups, // boolean - Loading state
error, // string | null - Error state
groupBy, // IGroupBy - Current grouping method
search, // string | null - Search filter
archived, // boolean - Show archived tasks
} = useSelector((state: RootState) => state.taskReducer);
```
### TaskGroup.tsx
Renders individual task groups with:
- **Collapsible Headers:** Expand/collapse functionality
- **Progress Indicators:** Visual completion progress
- **Drop Zones:** Accept dropped tasks from other groups
- **Group Statistics:** Task counts and completion rates
#### Key Props
```typescript
interface TaskGroupProps {
group: ITaskListGroup; // Group data with tasks
projectId: string; // Project context
currentGrouping: IGroupBy; // Current grouping mode
selectedTaskIds: string[]; // Selected task IDs
onAddTask?: (groupId: string) => void;
onToggleCollapse?: (groupId: string) => void;
}
```
### TaskRow.tsx
Individual task display featuring:
- **Rich Metadata:** Progress, assignees, labels, due dates
- **Drag Handles:** Sortable within and between groups
- **Selection:** Multi-select with checkboxes
- **Subtask Support:** Expandable hierarchy display
#### Key Props
```typescript
interface TaskRowProps {
task: IProjectTask; // Task data
projectId: string; // Project context
groupId: string; // Parent group ID
currentGrouping: IGroupBy; // Current grouping mode
isSelected: boolean; // Selection state
isDragOverlay?: boolean; // Drag overlay rendering
index?: number; // Position in group
onSelect?: (taskId: string, selected: boolean) => void;
onToggleSubtasks?: (taskId: string) => void;
}
```
## State Management
### Redux Integration
The system uses existing WorkLenz Redux patterns:
```typescript
// Primary slice used
import {
fetchTaskGroups, // Async thunk for loading data
reorderTasks, // Update task order/group
setGroup, // Change grouping method
updateTaskStatus, // Update individual task status
updateTaskPriority, // Update individual task priority
// ... other existing actions
} from '@/features/tasks/tasks.slice';
```
### Data Flow
1. **Component Mount:** `TaskListBoard` dispatches `fetchTaskGroups(projectId)`
2. **Group Changes:** `setGroup(newGroupBy)` triggers data reorganization
3. **Drag Operations:** `reorderTasks()` updates task positions and properties
4. **Real-time Updates:** WebSocket events update Redux state automatically
## Drag and Drop Implementation
### DnD Kit Integration
Uses @dnd-kit for modern, accessible drag-and-drop:
```typescript
// Sensors for different input methods
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 8 }
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates
})
);
```
### Drag Event Handling
```typescript
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
// Determine source and target
const sourceGroup = findTaskGroup(active.id);
const targetGroup = findTargetGroup(over?.id);
// Update task arrays and dispatch changes
dispatch(reorderTasks({
activeGroupId: sourceGroup.id,
overGroupId: targetGroup.id,
fromIndex: sourceIndex,
toIndex: targetIndex,
task: movedTask,
updatedSourceTasks,
updatedTargetTasks,
}));
};
```
### Smart Property Updates
When tasks are moved between groups, properties update automatically:
- **Status Grouping:** Moving to "Done" group sets task status to "done"
- **Priority Grouping:** Moving to "High" group sets task priority to "high"
- **Phase Grouping:** Moving to "Testing" group sets task phase to "testing"
## Bulk Operations
### Selection State Management
```typescript
// Local state for task selection
const [selectedTaskIds, setSelectedTaskIds] = useState<string[]>([]);
// Selection handlers
const handleTaskSelect = (taskId: string, selected: boolean) => {
if (selected) {
setSelectedTaskIds(prev => [...prev, taskId]);
} else {
setSelectedTaskIds(prev => prev.filter(id => id !== taskId));
}
};
```
### Context-Aware Actions
Bulk actions adapt to current grouping:
```typescript
// Only show status changes when not grouped by status
{currentGrouping !== 'status' && (
<Dropdown overlay={statusMenu}>
<Button>Change Status</Button>
</Dropdown>
)}
```
## Performance Optimizations
### Memoized Selectors
```typescript
// Expensive group calculations are memoized
const taskGroups = useMemo(() => {
return createGroupsFromTasks(tasks, currentGrouping);
}, [tasks, currentGrouping]);
```
### Virtual Scrolling Ready
For large datasets, the system is prepared for react-window integration:
```typescript
// Large group detection
const shouldVirtualize = group.tasks.length > 100;
return shouldVirtualize ? (
<VirtualizedTaskList tasks={group.tasks} />
) : (
<StandardTaskList tasks={group.tasks} />
);
```
### Optimistic Updates
UI updates immediately while API calls process in background:
```typescript
// Immediate UI update
dispatch(updateTaskStatusOptimistically(taskId, newStatus));
// API call with rollback on error
try {
await updateTaskStatus(taskId, newStatus);
} catch (error) {
dispatch(rollbackTaskStatusUpdate(taskId));
}
```
## Responsive Design
### Breakpoint Strategy
```css
/* Mobile-first responsive design */
.task-row {
padding: 12px;
}
@media (min-width: 768px) {
.task-row {
padding: 16px;
}
}
@media (min-width: 1024px) {
.task-row {
padding: 20px;
}
}
```
### Progressive Enhancement
- **Mobile:** Essential information only
- **Tablet:** Additional metadata visible
- **Desktop:** Full feature set with optimal layout
## Accessibility
### ARIA Implementation
```typescript
// Proper ARIA labels for screen readers
<div
role="button"
aria-label={`Move task ${task.name}`}
tabIndex={0}
{...dragHandleProps}
>
<DragOutlined />
</div>
```
### Keyboard Navigation
- **Tab:** Navigate between elements
- **Space:** Select/deselect tasks
- **Enter:** Activate buttons
- **Arrows:** Navigate sortable lists with keyboard sensor
### Focus Management
```typescript
// Maintain focus during dynamic updates
useEffect(() => {
if (shouldFocusTask) {
taskRef.current?.focus();
}
}, [taskGroups]);
```
## WebSocket Integration
### Real-time Updates
The system subscribes to existing WorkLenz WebSocket events:
```typescript
// Socket event handlers (existing WorkLenz patterns)
socket.on('TASK_STATUS_CHANGED', (data) => {
dispatch(updateTaskStatus(data));
});
socket.on('TASK_PROGRESS_UPDATED', (data) => {
dispatch(updateTaskProgress(data));
});
```
### Live Collaboration
- Multiple users can work simultaneously
- Changes appear in real-time
- Conflict resolution through server-side validation
## API Integration
### Existing Endpoints Used
```typescript
// Uses existing WorkLenz API services
import { tasksApiService } from '@/api/tasks/tasks.api.service';
// Task data fetching
tasksApiService.getTaskList(config);
// Task updates
tasksApiService.updateTask(taskId, changes);
// Bulk operations
tasksApiService.bulkUpdateTasks(taskIds, changes);
```
### Error Handling
```typescript
try {
await dispatch(fetchTaskGroups(projectId));
} catch (error) {
// Display user-friendly error message
message.error('Failed to load tasks. Please try again.');
logger.error('Task loading error:', error);
}
```
## Testing Strategy
### Component Testing
```typescript
// Example test structure
describe('TaskListBoard', () => {
it('should render task groups correctly', () => {
const mockTasks = generateMockTasks(10);
render(<TaskListBoard projectId="test-project" />);
expect(screen.getByText('Tasks (10)')).toBeInTheDocument();
});
it('should handle drag and drop operations', async () => {
// Test drag and drop functionality
});
});
```
### Integration Testing
- Redux state management
- API service integration
- WebSocket event handling
- Drag and drop operations
## Development Guidelines
### Code Organization
- Follow existing WorkLenz patterns
- Use TypeScript strictly
- Implement proper error boundaries
- Maintain accessibility standards
### Performance Considerations
- Memoize expensive calculations
- Implement virtual scrolling for large datasets
- Debounce user input operations
- Optimize re-render cycles
### Styling Standards
- Use existing Ant Design components
- Follow WorkLenz design system
- Implement responsive breakpoints
- Maintain dark mode compatibility
## Future Enhancements
### Planned Features
- Custom column integration
- Advanced filtering capabilities
- Kanban board view
- Enhanced time tracking
- Task templates
### Extension Points
The system is designed for easy extension:
```typescript
// Plugin architecture ready
interface TaskViewPlugin {
name: string;
component: React.ComponentType;
supportedGroupings: IGroupBy[];
}
const plugins: TaskViewPlugin[] = [
{ name: 'kanban', component: KanbanView, supportedGroupings: ['status'] },
{ name: 'timeline', component: TimelineView, supportedGroupings: ['phase'] },
];
```
## Deployment Considerations
### Bundle Size
- Tree-shake unused dependencies
- Code-split large components
- Optimize asset loading
### Browser Compatibility
- Modern browsers (ES2020+)
- Graceful degradation for older browsers
- Progressive enhancement approach
### Performance Monitoring
- Track component render times
- Monitor API response times
- Measure user interaction latency

View File

@@ -0,0 +1,275 @@
# Enhanced Task Management: User Guide
## What Is Enhanced Task Management?
The Enhanced Task Management system provides a modern, grouped view of your tasks with advanced features like drag-and-drop, bulk operations, and dynamic grouping. This system builds on WorkLenz's existing task infrastructure while offering improved productivity and organization tools.
## Why Use Enhanced Task Management?
- **Better Organization:** Group tasks by Status, Priority, or Phase for clearer project overview
- **Increased Productivity:** Bulk operations let you update multiple tasks at once
- **Intuitive Interface:** Drag-and-drop functionality makes task management feel natural
- **Rich Task Display:** See progress, assignees, labels, and due dates at a glance
- **Responsive Design:** Works seamlessly on desktop, tablet, and mobile devices
## Getting Started
### Accessing Enhanced Task Management
1. Navigate to your project workspace
2. Look for the enhanced task view option in your project interface
3. The system will display your tasks grouped by the current grouping method (default: Status)
### Understanding the Interface
The enhanced task management interface consists of several key areas:
- **Header Controls:** Task count, grouping selector, and action buttons
- **Task Groups:** Collapsible sections containing related tasks
- **Individual Tasks:** Rich task cards with metadata and actions
- **Bulk Action Bar:** Appears when multiple tasks are selected (blue bar)
## Task Grouping
### Available Grouping Options
You can organize your tasks using three different grouping methods:
#### 1. Status Grouping (Default)
Groups tasks by their current status:
- **To Do:** Tasks not yet started
- **Doing:** Tasks currently in progress
- **Done:** Completed tasks
#### 2. Priority Grouping
Groups tasks by their priority level:
- **Critical:** Highest priority, urgent tasks
- **High:** Important tasks requiring attention
- **Medium:** Standard priority tasks
- **Low:** Tasks that can be addressed later
#### 3. Phase Grouping
Groups tasks by project phases:
- **Planning:** Tasks in the planning stage
- **Development:** Implementation and development tasks
- **Testing:** Quality assurance and testing tasks
- **Deployment:** Release and deployment tasks
### Switching Between Groupings
1. Locate the "Group by" dropdown in the header controls
2. Select your preferred grouping method (Status, Priority, or Phase)
3. Tasks will automatically reorganize into the new groups
4. Your grouping preference is saved for future sessions
### Group Features
Each task group includes:
- **Color-coded headers** with visual indicators
- **Task count badges** showing the number of tasks in each group
- **Progress indicators** showing completion percentage
- **Collapse/expand functionality** to hide or show group contents
- **Add task buttons** to quickly create tasks in specific groups
## Drag and Drop
### Moving Tasks Within Groups
1. Hover over a task to reveal the drag handle (⋮⋮ icon)
2. Click and hold the drag handle
3. Drag the task to your desired position within the same group
4. Release to drop the task in its new position
### Moving Tasks Between Groups
1. Click and hold the drag handle on any task
2. Drag the task over a different group
3. The target group will highlight to show it can accept the task
4. Release to drop the task into the new group
5. The task's properties (status, priority, or phase) will automatically update
### Drag and Drop Benefits
- **Instant Updates:** Task properties change automatically when moved between groups
- **Visual Feedback:** Clear indicators show where tasks can be dropped
- **Keyboard Accessible:** Alternative keyboard controls for accessibility
- **Mobile Friendly:** Touch-friendly drag operations on mobile devices
## Multi-Select and Bulk Operations
### Selecting Tasks
You can select multiple tasks using several methods:
#### Individual Selection
- Click the checkbox next to any task to select it
- Click again to deselect
#### Range Selection
- Select the first task in your desired range
- Hold Shift and click the last task in the range
- All tasks between the first and last will be selected
#### Multiple Selection
- Hold Ctrl (or Cmd on Mac) while clicking tasks
- This allows you to select non-consecutive tasks
### Bulk Actions
When you have tasks selected, a blue bulk action bar appears with these options:
#### Change Status (when not grouped by Status)
- Update the status of all selected tasks at once
- Choose from available status options in your project
#### Set Priority (when not grouped by Priority)
- Assign the same priority level to all selected tasks
- Options include Critical, High, Medium, and Low
#### More Actions
Additional bulk operations include:
- **Assign to Member:** Add team members to multiple tasks
- **Add Labels:** Apply labels to selected tasks
- **Archive Tasks:** Move multiple tasks to archive
#### Delete Tasks
- Permanently remove multiple tasks at once
- Confirmation dialog prevents accidental deletions
### Bulk Action Tips
- The bulk action bar only shows relevant options based on your current grouping
- You can clear your selection at any time using the "Clear" button
- Bulk operations provide immediate feedback and can be undone if needed
## Task Display Features
### Rich Task Information
Each task displays comprehensive information:
#### Basic Information
- **Task Key:** Unique identifier (e.g., PROJ-123)
- **Task Name:** Clear, descriptive title
- **Description:** Additional details when available
#### Visual Indicators
- **Progress Bar:** Shows completion percentage (0-100%)
- **Priority Indicator:** Color-coded dot showing task importance
- **Status Color:** Left border color indicates current status
#### Team and Collaboration
- **Assignee Avatars:** Profile pictures of assigned team members (up to 3 visible)
- **Labels:** Color-coded tags for categorization
- **Comment Count:** Number of comments and discussions
- **Attachment Count:** Number of files attached to the task
#### Timing Information
- **Due Dates:** When tasks are scheduled to complete
- Red text: Overdue tasks
- Orange text: Due today or within 3 days
- Gray text: Future due dates
- **Time Tracking:** Estimated vs. logged time when available
### Subtask Support
Tasks with subtasks include additional features:
#### Expanding Subtasks
- Click the "+X" button next to task names to expand subtasks
- Subtasks appear indented below the parent task
- Click "X" to collapse subtasks
#### Subtask Progress
- Parent task progress reflects completion of all subtasks
- Individual subtask progress is visible when expanded
- Subtask counts show total number of child tasks
## Advanced Features
### Real-time Updates
- Changes made by team members appear instantly
- Live collaboration with multiple users
- WebSocket connections ensure data synchronization
### Search and Filtering
- Use existing project search and filter capabilities
- Enhanced task management respects current filter settings
- Search results maintain grouping organization
### Responsive Design
The interface adapts to different screen sizes:
#### Desktop (Large Screens)
- Full feature set with all metadata visible
- Optimal drag-and-drop experience
- Multi-column layouts where appropriate
#### Tablet (Medium Screens)
- Condensed but functional interface
- Touch-friendly interactions
- Simplified metadata display
#### Mobile (Small Screens)
- Stacked layout for easy navigation
- Large touch targets for selections
- Essential information prioritized
## Best Practices
### Organizing Your Tasks
1. **Choose the Right Grouping:** Select the grouping method that best fits your workflow
2. **Use Labels Consistently:** Apply meaningful labels for better categorization
3. **Keep Groups Balanced:** Avoid having too many tasks in a single group
4. **Regular Maintenance:** Review and update task organization periodically
### Collaboration Tips
1. **Clear Task Names:** Use descriptive titles that everyone understands
2. **Proper Assignment:** Assign tasks to appropriate team members
3. **Progress Updates:** Keep progress percentages current for accurate project tracking
4. **Use Comments:** Communicate about tasks using the comment system
### Productivity Techniques
1. **Batch Similar Operations:** Use bulk actions for efficiency
2. **Prioritize Effectively:** Use priority grouping during planning phases
3. **Track Progress:** Monitor completion rates using group progress indicators
4. **Plan Ahead:** Use due dates and time estimates for better scheduling
## Keyboard Shortcuts
### Navigation
- **Tab:** Move focus between elements
- **Enter:** Activate focused button or link
- **Esc:** Close open dialogs or clear selections
### Selection
- **Space:** Select/deselect focused task
- **Shift + Click:** Range selection
- **Ctrl + Click:** Multi-selection (Cmd + Click on Mac)
### Actions
- **Delete:** Remove selected tasks (with confirmation)
- **Ctrl + A:** Select all visible tasks (Cmd + A on Mac)
## Troubleshooting
### Common Issues
#### Tasks Not Moving Between Groups
- Ensure you have edit permissions for the tasks
- Check that you're dragging from the drag handle (⋮⋮ icon)
- Verify the target group allows the task type
#### Bulk Actions Not Working
- Confirm tasks are actually selected (checkboxes checked)
- Ensure you have appropriate permissions
- Check that the action is available for your current grouping
#### Missing Task Information
- Some metadata may be hidden on smaller screens
- Try expanding to full screen or using desktop view
- Check that task has the required information (assignees, labels, etc.)
### Performance Tips
- For projects with hundreds of tasks, consider using filters to reduce visible tasks
- Collapse groups you're not actively working with
- Clear selections when not performing bulk operations
## Getting Help
- Contact your workspace administrator for permission-related issues
- Check the main WorkLenz documentation for general task management help
- Report bugs or feature requests through your organization's support channels
## What's New
This enhanced task management system builds on WorkLenz's solid foundation while adding:
- Modern drag-and-drop interfaces
- Flexible grouping options
- Powerful bulk operation capabilities
- Rich visual task displays
- Mobile-responsive design
- Improved accessibility features

View File

@@ -73,6 +73,11 @@ import timeReportsOverviewReducer from '@features/reporting/time-reports/time-re
import roadmapReducer from '../features/roadmap/roadmap-slice';
import teamMembersReducer from '@features/team-members/team-members.slice';
import groupByFilterDropdownReducer from '../features/group-by-filter-dropdown/group-by-filter-dropdown-slice';
// Task Management System
import taskManagementReducer from '@features/task-management/task-management.slice';
import groupingReducer from '@features/task-management/grouping.slice';
import selectionReducer from '@features/task-management/selection.slice';
import homePageApiService from '@/api/home-page/home-page.api.service';
import { projectsApi } from '@/api/projects/projects.v1.api.service';
@@ -159,6 +164,11 @@ export const store = configureStore({
roadmapReducer: roadmapReducer,
groupByFilterDropdownReducer: groupByFilterDropdownReducer,
timeReportsOverviewReducer: timeReportsOverviewReducer,
// Task Management System
taskManagement: taskManagementReducer,
grouping: groupingReducer,
taskManagementSelection: selectionReducer,
},
});

View File

@@ -0,0 +1,174 @@
import React from 'react';
import { Card, Button, Space, Typography, Dropdown, Menu, Popconfirm, message } from 'antd';
import {
DeleteOutlined,
EditOutlined,
TagOutlined,
UserOutlined,
CheckOutlined,
CloseOutlined,
MoreOutlined,
} from '@ant-design/icons';
import { useDispatch, useSelector } from 'react-redux';
import { IGroupBy, bulkUpdateTasks, bulkDeleteTasks } from '@/features/tasks/tasks.slice';
import { AppDispatch, RootState } from '@/app/store';
const { Text } = Typography;
interface BulkActionBarProps {
selectedTaskIds: string[];
totalSelected: number;
currentGrouping: IGroupBy;
projectId: string;
onClearSelection?: () => void;
}
const BulkActionBar: React.FC<BulkActionBarProps> = ({
selectedTaskIds,
totalSelected,
currentGrouping,
projectId,
onClearSelection,
}) => {
const dispatch = useDispatch<AppDispatch>();
const { statuses, priorities } = useSelector((state: RootState) => state.taskReducer);
const handleBulkStatusChange = (statusId: string) => {
// dispatch(bulkUpdateTasks({ ids: selectedTaskIds, changes: { status: statusId } }));
message.success(`Updated ${totalSelected} tasks`);
onClearSelection?.();
};
const handleBulkPriorityChange = (priority: string) => {
// dispatch(bulkUpdateTasks({ ids: selectedTaskIds, changes: { priority } }));
message.success(`Updated ${totalSelected} tasks`);
onClearSelection?.();
};
const handleBulkDelete = () => {
// dispatch(bulkDeleteTasks(selectedTaskIds));
message.success(`Deleted ${totalSelected} tasks`);
onClearSelection?.();
};
const statusMenu = (
<Menu
onClick={({ key }) => handleBulkStatusChange(key)}
items={statuses.map(status => ({
key: status.id!,
label: (
<div className="flex items-center space-x-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: status.color_code }}
/>
<span>{status.name}</span>
</div>
),
}))}
/>
);
const priorityMenu = (
<Menu
onClick={({ key }) => handleBulkPriorityChange(key)}
items={[
{ key: 'critical', label: 'Critical', icon: <div className="w-2 h-2 rounded-full bg-red-500" /> },
{ key: 'high', label: 'High', icon: <div className="w-2 h-2 rounded-full bg-orange-500" /> },
{ key: 'medium', label: 'Medium', icon: <div className="w-2 h-2 rounded-full bg-yellow-500" /> },
{ key: 'low', label: 'Low', icon: <div className="w-2 h-2 rounded-full bg-green-500" /> },
]}
/>
);
const moreActionsMenu = (
<Menu
items={[
{
key: 'assign',
label: 'Assign to member',
icon: <UserOutlined />,
},
{
key: 'labels',
label: 'Add labels',
icon: <TagOutlined />,
},
{
key: 'archive',
label: 'Archive tasks',
icon: <EditOutlined />,
},
]}
/>
);
return (
<Card
size="small"
className="mb-4 bg-blue-50 border-blue-200"
styles={{ body: { padding: '8px 16px' } }}
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Text strong className="text-blue-700">
{totalSelected} task{totalSelected > 1 ? 's' : ''} selected
</Text>
</div>
<Space>
{/* Status Change */}
{currentGrouping !== 'status' && (
<Dropdown overlay={statusMenu} trigger={['click']}>
<Button size="small" icon={<CheckOutlined />}>
Change Status
</Button>
</Dropdown>
)}
{/* Priority Change */}
{currentGrouping !== 'priority' && (
<Dropdown overlay={priorityMenu} trigger={['click']}>
<Button size="small" icon={<EditOutlined />}>
Set Priority
</Button>
</Dropdown>
)}
{/* More Actions */}
<Dropdown overlay={moreActionsMenu} trigger={['click']}>
<Button size="small" icon={<MoreOutlined />}>
More Actions
</Button>
</Dropdown>
{/* Delete */}
<Popconfirm
title={`Delete ${totalSelected} task${totalSelected > 1 ? 's' : ''}?`}
description="This action cannot be undone."
onConfirm={handleBulkDelete}
okText="Delete"
cancelText="Cancel"
okType="danger"
>
<Button size="small" danger icon={<DeleteOutlined />}>
Delete
</Button>
</Popconfirm>
{/* Clear Selection */}
<Button
size="small"
icon={<CloseOutlined />}
onClick={onClearSelection}
title="Clear selection"
>
Clear
</Button>
</Space>
</div>
</Card>
);
};
export default BulkActionBar;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { Select, Typography } from 'antd';
import { IGroupBy } from '@/features/tasks/tasks.slice';
import { IGroupByOption } from '@/types/tasks/taskList.types';
const { Text } = Typography;
const { Option } = Select;
interface GroupingSelectorProps {
currentGrouping: IGroupBy;
onChange: (groupBy: IGroupBy) => void;
options: IGroupByOption[];
disabled?: boolean;
}
const GroupingSelector: React.FC<GroupingSelectorProps> = ({
currentGrouping,
onChange,
options,
disabled = false,
}) => {
return (
<div className="flex items-center space-x-2">
<Text className="text-sm text-gray-600">Group by:</Text>
<Select
value={currentGrouping}
onChange={onChange}
disabled={disabled}
size="small"
style={{ minWidth: 100 }}
className="capitalize"
>
{options.map((option) => (
<Option key={option.value} value={option.value} className="capitalize">
{option.label}
</Option>
))}
</Select>
</div>
);
};
export default GroupingSelector;

View File

@@ -0,0 +1,444 @@
import React, { useState } from 'react';
import { useDroppable } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { useSelector } from 'react-redux';
import { Button, Typography } from 'antd';
import { PlusOutlined, RightOutlined, DownOutlined } from '@ant-design/icons';
import { ITaskListGroup } from '@/types/tasks/taskList.types';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { IGroupBy, COLUMN_KEYS } from '@/features/tasks/tasks.slice';
import { RootState } from '@/app/store';
import TaskRow from './TaskRow';
import AddTaskListRow from '@/pages/projects/projectView/taskList/task-list-table/task-list-table-rows/add-task-list-row';
const { Text } = Typography;
interface TaskGroupProps {
group: ITaskListGroup;
projectId: string;
currentGrouping: IGroupBy;
selectedTaskIds: string[];
onAddTask?: (groupId: string) => void;
onToggleCollapse?: (groupId: string) => void;
onSelectTask?: (taskId: string, selected: boolean) => void;
onToggleSubtasks?: (taskId: string) => void;
}
const TaskGroup: React.FC<TaskGroupProps> = ({
group,
projectId,
currentGrouping,
selectedTaskIds,
onAddTask,
onToggleCollapse,
onSelectTask,
onToggleSubtasks,
}) => {
const [isCollapsed, setIsCollapsed] = useState(false);
const { setNodeRef, isOver } = useDroppable({
id: group.id,
data: {
type: 'group',
groupId: group.id,
},
});
// Get column visibility from Redux store
const columns = useSelector((state: RootState) => state.taskReducer.columns);
// Helper function to check if a column is visible
const isColumnVisible = (columnKey: string) => {
const column = columns.find(col => col.key === columnKey);
return column ? column.pinned : true; // Default to visible if column not found
};
// Get task IDs for sortable context
const taskIds = group.tasks.map(task => task.id!);
// Calculate group statistics
const completedTasks = group.tasks.filter(
task => task.status_category?.is_done || task.complete_ratio === 100
).length;
const totalTasks = group.tasks.length;
const completionRate = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
// Get group color based on grouping type
const getGroupColor = () => {
if (group.color_code) return group.color_code;
// Fallback colors based on group value
switch (currentGrouping) {
case 'status':
return group.id === 'todo' ? '#faad14' : group.id === 'doing' ? '#1890ff' : '#52c41a';
case 'priority':
return group.id === 'critical'
? '#ff4d4f'
: group.id === 'high'
? '#fa8c16'
: group.id === 'medium'
? '#faad14'
: '#52c41a';
case 'phase':
return '#722ed1';
default:
return '#d9d9d9';
}
};
const handleToggleCollapse = () => {
setIsCollapsed(!isCollapsed);
onToggleCollapse?.(group.id);
};
const handleAddTask = () => {
onAddTask?.(group.id);
};
return (
<div
ref={setNodeRef}
className={`task-group ${isOver ? 'drag-over' : ''}`}
style={{
backgroundColor: isOver ? '#f0f8ff' : undefined,
}}
>
{/* Group Header Row */}
<div className="task-group-header">
<div className="task-group-header-row">
<div
className="task-group-header-content"
style={{ backgroundColor: getGroupColor() }}
>
<Button
type="text"
size="small"
icon={isCollapsed ? <RightOutlined /> : <DownOutlined />}
onClick={handleToggleCollapse}
className="task-group-header-button"
/>
<Text strong className="task-group-header-text">
{group.name} ({totalTasks})
</Text>
</div>
</div>
</div>
{/* Column Headers */}
{!isCollapsed && totalTasks > 0 && (
<div className="task-group-column-headers">
<div className="task-group-column-headers-row">
<div className="task-table-fixed-columns">
<div
className="task-table-cell task-table-header-cell"
style={{ width: '40px' }}
></div>
<div
className="task-table-cell task-table-header-cell"
style={{ width: '40px' }}
></div>
<div className="task-table-cell task-table-header-cell" style={{ width: '80px' }}>
<Text className="column-header-text">Key</Text>
</div>
<div className="task-table-cell task-table-header-cell" style={{ width: '475px' }}>
<Text className="column-header-text">Task</Text>
</div>
</div>
<div className="task-table-scrollable-columns">
{isColumnVisible(COLUMN_KEYS.PROGRESS) && (
<div className="task-table-cell task-table-header-cell" style={{ width: '90px' }}>
<Text className="column-header-text">Progress</Text>
</div>
)}
{isColumnVisible(COLUMN_KEYS.ASSIGNEES) && (
<div className="task-table-cell task-table-header-cell" style={{ width: '150px' }}>
<Text className="column-header-text">Members</Text>
</div>
)}
{isColumnVisible(COLUMN_KEYS.LABELS) && (
<div className="task-table-cell task-table-header-cell" style={{ width: '150px' }}>
<Text className="column-header-text">Labels</Text>
</div>
)}
{isColumnVisible(COLUMN_KEYS.STATUS) && (
<div className="task-table-cell task-table-header-cell" style={{ width: '100px' }}>
<Text className="column-header-text">Status</Text>
</div>
)}
{isColumnVisible(COLUMN_KEYS.PRIORITY) && (
<div className="task-table-cell task-table-header-cell" style={{ width: '100px' }}>
<Text className="column-header-text">Priority</Text>
</div>
)}
{isColumnVisible(COLUMN_KEYS.TIME_TRACKING) && (
<div className="task-table-cell task-table-header-cell" style={{ width: '120px' }}>
<Text className="column-header-text">Time Tracking</Text>
</div>
)}
</div>
</div>
</div>
)}
{/* Tasks List */}
{!isCollapsed && (
<div className="task-group-body">
{group.tasks.length === 0 ? (
<div className="task-group-empty">
<div className="task-table-fixed-columns">
<div style={{ width: '380px', padding: '20px 12px' }}>
<div className="text-center text-gray-500">
<Text type="secondary">No tasks in this group</Text>
<br />
<Button
type="link"
icon={<PlusOutlined />}
onClick={handleAddTask}
className="mt-2"
>
Add first task
</Button>
</div>
</div>
</div>
</div>
) : (
<SortableContext items={taskIds} strategy={verticalListSortingStrategy}>
<div className="task-group-tasks">
{group.tasks.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}
/>
))}
</div>
</SortableContext>
)}
{/* Add Task Row - Always show when not collapsed */}
<div className="task-group-add-task">
<AddTaskListRow groupId={group.id} />
</div>
</div>
)}
<style>{`
.task-group {
border: 1px solid var(--task-border-primary, #e8e8e8);
border-radius: 8px;
margin-bottom: 16px;
background: var(--task-bg-primary, white);
box-shadow: 0 1px 3px var(--task-shadow, rgba(0, 0, 0, 0.1));
overflow-x: auto;
overflow-y: visible;
transition: all 0.3s ease;
position: relative;
}
.task-group:last-child {
margin-bottom: 0;
}
.task-group-header {
background: var(--task-bg-primary, white);
transition: background-color 0.3s ease;
}
.task-group-header-row {
display: inline-flex;
height: auto;
max-height: none;
overflow: hidden;
}
.task-group-header-content {
display: inline-flex;
align-items: center;
padding: 8px 12px;
border-radius: 6px;
background-color: #f0f0f0;
color: white;
font-weight: 500;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.task-group-header-button {
color: white !important;
padding: 0 !important;
width: 16px !important;
height: 16px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin-right: 8px !important;
border: none !important;
background: transparent !important;
}
.task-group-header-button:hover {
background: rgba(255, 255, 255, 0.2) !important;
border-radius: 2px !important;
}
.task-group-header-text {
color: white !important;
font-size: 13px !important;
font-weight: 600 !important;
margin: 0 !important;
}
.task-group-progress {
display: flex;
height: 20px;
align-items: center;
background: var(--task-bg-tertiary, #f8f9fa);
border-bottom: 1px solid var(--task-border-primary, #e8e8e8);
transition: background-color 0.3s ease;
}
.task-group-column-headers {
background: var(--task-bg-secondary, #f5f5f5);
border-bottom: 1px solid var(--task-border-tertiary, #d9d9d9);
transition: background-color 0.3s ease;
}
.task-group-column-headers-row {
display: flex;
height: 40px;
max-height: 40px;
overflow: visible;
position: relative;
min-width: 1200px; /* Ensure minimum width for all columns */
}
.task-table-header-cell {
background: var(--task-bg-secondary, #f5f5f5);
font-weight: 600;
color: var(--task-text-secondary, #595959);
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--task-border-tertiary, #d9d9d9);
height: 32px;
max-height: 32px;
overflow: hidden;
transition: all 0.3s ease;
}
.column-header-text {
font-size: 11px;
font-weight: 600;
color: var(--task-text-secondary, #595959);
text-transform: uppercase;
letter-spacing: 0.5px;
transition: color 0.3s ease;
}
.task-group-body {
background: var(--task-bg-primary, white);
transition: background-color 0.3s ease;
overflow: visible;
position: relative;
}
.task-group-empty {
display: flex;
height: 80px;
align-items: center;
background: var(--task-bg-primary, white);
transition: background-color 0.3s ease;
}
.task-group-tasks {
background: var(--task-bg-primary, white);
transition: background-color 0.3s ease;
overflow: visible;
position: relative;
}
.task-group-add-task {
background: var(--task-bg-primary, white);
border-top: 1px solid var(--task-border-secondary, #f0f0f0);
transition: all 0.3s ease;
padding: 0 12px;
width: 100%;
min-height: 40px;
display: flex;
align-items: center;
}
.task-group-add-task:hover {
background: var(--task-hover-bg, #fafafa);
}
.task-table-fixed-columns {
display: flex;
background: var(--task-bg-secondary, #f5f5f5);
position: sticky;
left: 0;
z-index: 11;
border-right: 2px solid var(--task-border-primary, #e8e8e8);
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.task-table-scrollable-columns {
display: flex;
flex: 1;
min-width: 0;
}
.task-table-cell {
display: flex;
align-items: center;
padding: 0 12px;
border-right: 1px solid var(--task-border-secondary, #f0f0f0);
font-size: 12px;
white-space: nowrap;
height: 40px;
max-height: 40px;
min-height: 40px;
overflow: hidden;
color: var(--task-text-primary, #262626);
transition: all 0.3s ease;
}
.task-table-cell:last-child {
border-right: none;
}
.drag-over {
background-color: var(--task-drag-over-bg, #f0f8ff) !important;
border-color: var(--task-drag-over-border, #40a9ff) !important;
}
/* Ensure buttons and components fit within row height */
.task-group .ant-btn {
height: auto;
max-height: 32px;
line-height: 1.2;
}
.task-group .ant-badge {
height: auto;
line-height: 1.2;
}
/* Dark mode specific adjustments */
.dark .task-group,
[data-theme="dark"] .task-group {
box-shadow: 0 1px 3px var(--task-shadow, rgba(0, 0, 0, 0.3));
}
`}</style>
</div>
);
};
export default TaskGroup;

View File

@@ -0,0 +1,359 @@
import React, { useEffect, useState, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
DndContext,
DragOverlay,
DragStartEvent,
DragEndEvent,
DragOverEvent,
closestCorners,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
sortableKeyboardCoordinates,
} from '@dnd-kit/sortable';
import { Card, Spin, Empty } from 'antd';
import { RootState } from '@/app/store';
import {
IGroupBy,
setGroup,
fetchTaskGroups,
reorderTasks,
} from '@/features/tasks/tasks.slice';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import TaskGroup from './TaskGroup';
import TaskRow from './TaskRow';
import BulkActionBar from './BulkActionBar';
import { AppDispatch } from '@/app/store';
// Import the TaskListFilters component
const TaskListFilters = React.lazy(() => import('@/pages/projects/projectView/taskList/task-list-filters/task-list-filters'));
interface TaskListBoardProps {
projectId: string;
className?: string;
}
interface DragState {
activeTask: IProjectTask | null;
activeGroupId: string | null;
}
const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = '' }) => {
const dispatch = useDispatch<AppDispatch>();
const [dragState, setDragState] = useState<DragState>({
activeTask: null,
activeGroupId: null,
});
// Redux selectors
const {
taskGroups,
loadingGroups,
error,
groupBy,
search,
archived,
} = useSelector((state: RootState) => state.taskReducer);
// Selection state
const [selectedTaskIds, setSelectedTaskIds] = useState<string[]>([]);
// Drag and Drop sensors
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
// Fetch task groups when component mounts or dependencies change
useEffect(() => {
if (projectId) {
dispatch(fetchTaskGroups(projectId));
}
}, [dispatch, projectId, groupBy, search, archived]);
// Memoized calculations
const allTaskIds = useMemo(() => {
return taskGroups.flatMap(group => group.tasks.map(task => task.id!));
}, [taskGroups]);
const totalTasksCount = useMemo(() => {
return taskGroups.reduce((sum, group) => sum + group.tasks.length, 0);
}, [taskGroups]);
const hasSelection = selectedTaskIds.length > 0;
// Handlers
const handleGroupingChange = (newGroupBy: IGroupBy) => {
dispatch(setGroup(newGroupBy));
};
const handleDragStart = (event: DragStartEvent) => {
const { active } = event;
const taskId = active.id as string;
// Find the task and its group
let activeTask: IProjectTask | null = null;
let activeGroupId: string | null = null;
for (const group of taskGroups) {
const task = group.tasks.find(t => t.id === taskId);
if (task) {
activeTask = task;
activeGroupId = group.id;
break;
}
}
setDragState({
activeTask,
activeGroupId,
});
};
const handleDragOver = (event: DragOverEvent) => {
// Handle drag over logic if needed for visual feedback
};
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
setDragState({
activeTask: null,
activeGroupId: null,
});
if (!over || !dragState.activeTask || !dragState.activeGroupId) {
return;
}
const activeTaskId = active.id as string;
const overContainer = over.id as string;
// Determine if dropping on a group or task
const overGroup = taskGroups.find(g => g.id === overContainer);
let targetGroupId = overContainer;
let targetIndex = -1;
if (!overGroup) {
// Dropping on a task, find which group it belongs to
for (const group of taskGroups) {
const taskIndex = group.tasks.findIndex(t => t.id === overContainer);
if (taskIndex !== -1) {
targetGroupId = group.id;
targetIndex = taskIndex;
break;
}
}
}
const sourceGroup = taskGroups.find(g => g.id === dragState.activeGroupId);
const targetGroup = taskGroups.find(g => g.id === targetGroupId);
if (!sourceGroup || !targetGroup) return;
const sourceIndex = sourceGroup.tasks.findIndex(t => t.id === activeTaskId);
if (sourceIndex === -1) return;
// Calculate new positions
const finalTargetIndex = targetIndex === -1 ? targetGroup.tasks.length : targetIndex;
// Create updated task arrays
const updatedSourceTasks = [...sourceGroup.tasks];
const [movedTask] = updatedSourceTasks.splice(sourceIndex, 1);
let updatedTargetTasks: IProjectTask[];
if (sourceGroup.id === targetGroup.id) {
// Moving within the same group
updatedTargetTasks = updatedSourceTasks;
updatedTargetTasks.splice(finalTargetIndex, 0, movedTask);
} else {
// Moving between different groups
updatedTargetTasks = [...targetGroup.tasks];
updatedTargetTasks.splice(finalTargetIndex, 0, movedTask);
}
// Dispatch the reorder action
dispatch(reorderTasks({
activeGroupId: sourceGroup.id,
overGroupId: targetGroup.id,
fromIndex: sourceIndex,
toIndex: finalTargetIndex,
task: movedTask,
updatedSourceTasks,
updatedTargetTasks,
}));
};
const handleSelectTask = (taskId: string, selected: boolean) => {
setSelectedTaskIds(prev => {
if (selected) {
return [...prev, taskId];
} else {
return prev.filter(id => id !== taskId);
}
});
};
const handleToggleSubtasks = (taskId: string) => {
// Implementation for toggling subtasks
console.log('Toggle subtasks for task:', taskId);
};
if (error) {
return (
<Card className={className}>
<Empty
description={`Error loading tasks: ${error}`}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
</Card>
);
}
return (
<div className={`task-list-board ${className}`}>
{/* Task Filters */}
<Card
size="small"
className="mb-4"
styles={{ body: { padding: '12px 16px' } }}
>
<React.Suspense fallback={<div>Loading filters...</div>}>
<TaskListFilters position="list" />
</React.Suspense>
</Card>
{/* Bulk Action Bar */}
{hasSelection && (
<BulkActionBar
selectedTaskIds={selectedTaskIds}
totalSelected={selectedTaskIds.length}
currentGrouping={groupBy}
projectId={projectId}
/>
)}
{/* Task Groups Container */}
<div className="task-groups-container">
{loadingGroups ? (
<Card>
<div className="flex justify-center items-center py-8">
<Spin size="large" />
</div>
</Card>
) : taskGroups.length === 0 ? (
<Card>
<Empty
description="No tasks found"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
</Card>
) : (
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
>
<div className="task-groups">
{taskGroups.map((group) => (
<TaskGroup
key={group.id}
group={group}
projectId={projectId}
currentGrouping={groupBy}
selectedTaskIds={selectedTaskIds}
onSelectTask={handleSelectTask}
onToggleSubtasks={handleToggleSubtasks}
/>
))}
</div>
<DragOverlay>
{dragState.activeTask ? (
<TaskRow
task={dragState.activeTask}
projectId={projectId}
groupId={dragState.activeGroupId!}
currentGrouping={groupBy}
isSelected={false}
isDragOverlay
/>
) : null}
</DragOverlay>
</DndContext>
)}
</div>
<style>{`
.task-groups-container {
max-height: calc(100vh - 300px);
overflow-y: auto;
overflow-x: visible;
padding: 8px 8px 8px 0;
border-radius: 8px;
transition: background-color 0.3s ease;
position: relative;
}
.task-groups {
min-width: fit-content;
position: relative;
}
/* Dark mode support */
:root {
--task-bg-primary: #ffffff;
--task-bg-secondary: #f5f5f5;
--task-bg-tertiary: #f8f9fa;
--task-border-primary: #e8e8e8;
--task-border-secondary: #f0f0f0;
--task-border-tertiary: #d9d9d9;
--task-text-primary: #262626;
--task-text-secondary: #595959;
--task-text-tertiary: #8c8c8c;
--task-shadow: rgba(0, 0, 0, 0.1);
--task-hover-bg: #fafafa;
--task-selected-bg: #e6f7ff;
--task-selected-border: #1890ff;
--task-drag-over-bg: #f0f8ff;
--task-drag-over-border: #40a9ff;
}
.dark .task-groups-container,
[data-theme="dark"] .task-groups-container {
--task-bg-primary: #1f1f1f;
--task-bg-secondary: #141414;
--task-bg-tertiary: #262626;
--task-border-primary: #303030;
--task-border-secondary: #404040;
--task-border-tertiary: #505050;
--task-text-primary: #ffffff;
--task-text-secondary: #d9d9d9;
--task-text-tertiary: #8c8c8c;
--task-shadow: rgba(0, 0, 0, 0.3);
--task-hover-bg: #2a2a2a;
--task-selected-bg: #1a2332;
--task-selected-border: #1890ff;
--task-drag-over-bg: #1a2332;
--task-drag-over-border: #40a9ff;
}
`}</style>
</div>
);
};
export default TaskListBoard;

View File

@@ -0,0 +1,652 @@
import React from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { useSelector } from 'react-redux';
import { Checkbox, Avatar, Tag, Progress, Typography, Space, Button, Tooltip } from 'antd';
import {
HolderOutlined,
EyeOutlined,
MessageOutlined,
PaperClipOutlined,
ClockCircleOutlined,
} from '@ant-design/icons';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { IGroupBy, COLUMN_KEYS } from '@/features/tasks/tasks.slice';
import { RootState } from '@/app/store';
const { Text } = Typography;
interface TaskRowProps {
task: IProjectTask;
projectId: string;
groupId: string;
currentGrouping: IGroupBy;
isSelected: boolean;
isDragOverlay?: boolean;
index?: number;
onSelect?: (taskId: string, selected: boolean) => void;
onToggleSubtasks?: (taskId: string) => void;
}
const TaskRow: React.FC<TaskRowProps> = ({
task,
projectId,
groupId,
currentGrouping,
isSelected,
isDragOverlay = false,
index,
onSelect,
onToggleSubtasks,
}) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id: task.id!,
data: {
type: 'task',
taskId: task.id,
groupId,
},
disabled: isDragOverlay,
});
// Get column visibility from Redux store
const columns = useSelector((state: RootState) => state.taskReducer.columns);
// Helper function to check if a column is visible
const isColumnVisible = (columnKey: string) => {
const column = columns.find(col => col.key === columnKey);
return column ? column.pinned : true; // Default to visible if column not found
};
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
const handleSelectChange = (checked: boolean) => {
onSelect?.(task.id!, checked);
};
const handleToggleSubtasks = () => {
onToggleSubtasks?.(task.id!);
};
// Format due date
const formatDueDate = (dateString?: string) => {
if (!dateString) return null;
const date = new Date(dateString);
const now = new Date();
const diffTime = date.getTime() - now.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 0) {
return { text: `${Math.abs(diffDays)}d overdue`, color: 'error' };
} else if (diffDays === 0) {
return { text: 'Due today', color: 'warning' };
} else if (diffDays <= 3) {
return { text: `Due in ${diffDays}d`, color: 'warning' };
} else {
return { text: `Due ${date.toLocaleDateString()}`, color: 'default' };
}
};
const dueDate = formatDueDate(task.end_date);
return (
<>
<div
ref={setNodeRef}
style={style}
className={`task-row ${isSelected ? 'task-row-selected' : ''} ${isDragOverlay ? 'task-row-drag-overlay' : ''}`}
>
<div className="task-row-content">
{/* Fixed Columns */}
<div className="task-table-fixed-columns">
{/* Drag Handle */}
<div className="task-table-cell task-table-cell-drag" style={{ width: '40px' }}>
<Button
type="text"
size="small"
icon={<HolderOutlined />}
className="drag-handle opacity-40 hover:opacity-100 cursor-grab active:cursor-grabbing"
{...attributes}
{...listeners}
/>
</div>
{/* Selection Checkbox */}
<div className="task-table-cell task-table-cell-checkbox" style={{ width: '40px' }}>
<Checkbox
checked={isSelected}
onChange={(e) => handleSelectChange(e.target.checked)}
/>
</div>
{/* Task Key */}
<div className="task-table-cell task-table-cell-key" style={{ width: '80px' }}>
{task.project_id && task.task_key && (
<Text code className="task-key">
{task.task_key}
</Text>
)}
</div>
{/* Task Name */}
<div className="task-table-cell task-table-cell-task" style={{ width: '475px' }}>
<div className="task-content">
<div className="task-header">
<Text
strong
className={`task-name ${task.complete_ratio === 100 ? 'task-completed' : ''}`}
>
{task.name}
</Text>
{task.sub_tasks_count && task.sub_tasks_count > 0 && (
<Button
type="text"
size="small"
onClick={handleToggleSubtasks}
className="subtask-toggle"
>
{task.show_sub_tasks ? '' : '+'} {task.sub_tasks_count}
</Button>
)}
</div>
</div>
</div>
</div>
{/* Scrollable Columns */}
<div className="task-table-scrollable-columns">
{/* Progress */}
{isColumnVisible(COLUMN_KEYS.PROGRESS) && (
<div className="task-table-cell" style={{ width: '90px' }}>
{task.complete_ratio !== undefined && task.complete_ratio >= 0 && (
<div className="task-progress">
<Progress
type="circle"
percent={task.complete_ratio}
size={32}
strokeColor={task.complete_ratio === 100 ? '#52c41a' : '#1890ff'}
strokeWidth={4}
showInfo={true}
format={(percent) => <span style={{ fontSize: '10px', fontWeight: '500' }}>{percent}%</span>}
/>
</div>
)}
</div>
)}
{/* Members */}
{isColumnVisible(COLUMN_KEYS.ASSIGNEES) && (
<div className="task-table-cell" style={{ width: '150px' }}>
{task.assignees && task.assignees.length > 0 && (
<Avatar.Group size="small" maxCount={3}>
{task.assignees.map((assignee) => (
<Tooltip key={assignee.id} title={assignee.name}>
<Avatar
size="small"
>
{assignee.name?.charAt(0)?.toUpperCase()}
</Avatar>
</Tooltip>
))}
</Avatar.Group>
)}
</div>
)}
{/* Labels */}
{isColumnVisible(COLUMN_KEYS.LABELS) && (
<div className="task-table-cell" style={{ width: '150px' }}>
{task.labels && task.labels.length > 0 && (
<div className="task-labels-column">
{task.labels.slice(0, 3).map((label) => (
<Tag
key={label.id}
className="task-label"
style={{
backgroundColor: label.color_code,
border: 'none',
color: 'white',
}}
>
{label.name}
</Tag>
))}
{task.labels.length > 3 && (
<Text type="secondary" className="task-labels-more">
+{task.labels.length - 3}
</Text>
)}
</div>
)}
</div>
)}
{/* Status */}
{isColumnVisible(COLUMN_KEYS.STATUS) && (
<div className="task-table-cell" style={{ width: '100px' }}>
{task.status_name && (
<div
className="task-status"
style={{
backgroundColor: task.status_color,
color: 'white',
}}
>
{task.status_name}
</div>
)}
</div>
)}
{/* Priority */}
{isColumnVisible(COLUMN_KEYS.PRIORITY) && (
<div className="task-table-cell" style={{ width: '100px' }}>
{task.priority_name && (
<div className="task-priority">
<div
className="task-priority-indicator"
style={{ backgroundColor: task.priority_color }}
/>
<Text className="task-priority-text">{task.priority_name}</Text>
</div>
)}
</div>
)}
{/* Time Tracking */}
{isColumnVisible(COLUMN_KEYS.TIME_TRACKING) && (
<div className="task-table-cell" style={{ width: '120px' }}>
<div className="task-time-tracking">
{task.time_spent_string && (
<div className="task-time-spent">
<ClockCircleOutlined className="task-time-icon" />
<Text className="task-time-text">{task.time_spent_string}</Text>
</div>
)}
{/* Task Indicators */}
<div className="task-indicators">
{task.comments_count && task.comments_count > 0 && (
<div className="task-indicator">
<MessageOutlined />
<span>{task.comments_count}</span>
</div>
)}
{task.attachments_count && task.attachments_count > 0 && (
<div className="task-indicator">
<PaperClipOutlined />
<span>{task.attachments_count}</span>
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
</div>
{/* Subtasks */}
{task.show_sub_tasks && task.sub_tasks && task.sub_tasks.length > 0 && (
<div className="task-subtasks">
{task.sub_tasks.map((subtask) => (
<TaskRow
key={subtask.id}
task={subtask}
projectId={projectId}
groupId={groupId}
currentGrouping={currentGrouping}
isSelected={isSelected}
onSelect={onSelect}
/>
))}
</div>
)}
<style>{`
.task-row {
border-bottom: 1px solid var(--task-border-secondary, #f0f0f0);
background: var(--task-bg-primary, white);
transition: all 0.3s ease;
}
.task-row:hover {
background-color: var(--task-hover-bg, #fafafa);
}
.task-row-selected {
background-color: var(--task-selected-bg, #e6f7ff);
border-left: 3px solid var(--task-selected-border, #1890ff);
}
.task-row-drag-overlay {
background: var(--task-bg-primary, white);
border: 1px solid var(--task-border-tertiary, #d9d9d9);
border-radius: 4px;
box-shadow: 0 4px 12px var(--task-shadow, rgba(0, 0, 0, 0.15));
}
.task-row-content {
display: flex;
height: 40px;
max-height: 40px;
overflow: visible;
position: relative;
min-width: 1200px; /* Ensure minimum width for all columns */
}
.task-table-fixed-columns {
display: flex;
background: var(--task-bg-primary, white);
position: sticky;
left: 0;
z-index: 10;
border-right: 2px solid var(--task-border-primary, #e8e8e8);
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.task-table-scrollable-columns {
display: flex;
flex: 1;
min-width: 0;
}
.task-table-cell {
display: flex;
align-items: center;
padding: 0 8px;
border-right: 1px solid var(--task-border-secondary, #f0f0f0);
font-size: 12px;
white-space: nowrap;
height: 40px;
max-height: 40px;
min-height: 40px;
overflow: hidden;
color: var(--task-text-primary, #262626);
transition: all 0.3s ease;
}
.task-table-cell:last-child {
border-right: none;
}
.task-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
overflow: hidden;
}
.task-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 1px;
height: 20px;
overflow: hidden;
}
.task-key {
font-size: 10px;
color: var(--task-text-tertiary, #666);
background: var(--task-bg-secondary, #f0f0f0);
padding: 1px 4px;
border-radius: 2px;
transition: all 0.3s ease;
}
.task-name {
font-size: 13px;
font-weight: 500;
color: var(--task-text-primary, #262626);
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.3s ease;
}
.task-completed {
text-decoration: line-through;
color: var(--task-text-tertiary, #8c8c8c);
}
.subtask-toggle {
font-size: 10px;
color: var(--task-text-tertiary, #666);
padding: 0 4px;
height: 16px;
line-height: 16px;
transition: color 0.3s ease;
}
.task-labels {
display: flex;
gap: 2px;
flex-wrap: nowrap;
overflow: hidden;
height: 18px;
align-items: center;
}
.task-label {
font-size: 10px;
padding: 0 4px;
height: 16px;
line-height: 16px;
border-radius: 2px;
margin: 0;
}
.task-label-small {
font-size: 9px;
padding: 0 3px;
height: 14px;
line-height: 14px;
border-radius: 2px;
margin: 0;
}
.task-labels-more {
font-size: 10px;
color: var(--task-text-tertiary, #8c8c8c);
transition: color 0.3s ease;
}
.task-progress {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.task-progress .ant-progress {
flex: 0 0 auto;
}
.task-progress-text {
font-size: 10px;
color: var(--task-text-tertiary, #666);
min-width: 24px;
transition: color 0.3s ease;
}
.task-labels-column {
display: flex;
gap: 2px;
flex-wrap: nowrap;
overflow: hidden;
height: 100%;
align-items: center;
}
.task-status {
font-size: 10px;
padding: 2px 6px;
border-radius: 2px;
font-weight: 500;
text-transform: uppercase;
}
.task-priority {
display: flex;
align-items: center;
gap: 4px;
}
.task-priority-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
}
.task-priority-text {
font-size: 11px;
color: var(--task-text-tertiary, #666);
transition: color 0.3s ease;
}
.task-time-tracking {
display: flex;
align-items: center;
gap: 8px;
height: 100%;
overflow: hidden;
}
.task-time-spent {
display: flex;
align-items: center;
gap: 2px;
}
.task-time-icon {
font-size: 10px;
color: var(--task-text-tertiary, #8c8c8c);
transition: color 0.3s ease;
}
.task-time-text {
font-size: 10px;
color: var(--task-text-tertiary, #666);
transition: color 0.3s ease;
}
.task-indicators {
display: flex;
gap: 6px;
}
.task-indicator {
display: flex;
align-items: center;
gap: 2px;
font-size: 10px;
color: var(--task-text-tertiary, #8c8c8c);
transition: color 0.3s ease;
}
.task-subtasks {
margin-left: 40px;
border-left: 2px solid var(--task-border-secondary, #f0f0f0);
transition: border-color 0.3s ease;
}
.drag-handle {
opacity: 0.4;
transition: opacity 0.2s;
}
.drag-handle:hover {
opacity: 1;
}
/* Ensure buttons and components fit within row height */
.task-row .ant-btn {
height: auto;
max-height: 24px;
padding: 0 4px;
line-height: 1.2;
}
.task-row .ant-checkbox-wrapper {
height: 24px;
align-items: center;
}
.task-row .ant-avatar-group {
height: 24px;
align-items: center;
}
.task-row .ant-avatar {
width: 24px !important;
height: 24px !important;
line-height: 24px !important;
font-size: 10px !important;
}
.task-row .ant-tag {
margin: 0;
padding: 0 4px;
height: 16px;
line-height: 16px;
border-radius: 2px;
}
.task-row .ant-progress {
margin: 0;
line-height: 1;
}
.task-row .ant-progress-line {
height: 6px !important;
}
.task-row .ant-progress-bg {
height: 6px !important;
}
/* Dark mode specific adjustments for Ant Design components */
.dark .task-row .ant-progress-bg,
[data-theme="dark"] .task-row .ant-progress-bg {
background-color: var(--task-border-primary, #303030) !important;
}
.dark .task-row .ant-checkbox-wrapper,
[data-theme="dark"] .task-row .ant-checkbox-wrapper {
color: var(--task-text-primary, #ffffff);
}
.dark .task-row .ant-btn,
[data-theme="dark"] .task-row .ant-btn {
color: var(--task-text-secondary, #d9d9d9);
border-color: transparent;
}
.dark .task-row .ant-btn:hover,
[data-theme="dark"] .task-row .ant-btn:hover {
color: var(--task-text-primary, #ffffff);
background-color: var(--task-hover-bg, #2a2a2a);
}
`}</style>
</>
);
};
export default TaskRow;

View File

@@ -0,0 +1,189 @@
import { createSlice, PayloadAction, createSelector } from '@reduxjs/toolkit';
import { GroupingState, TaskGroup } from '@/types/task-management.types';
import { RootState } from '@/app/store';
import { taskManagementSelectors } from './task-management.slice';
const initialState: GroupingState = {
currentGrouping: 'status',
customPhases: ['Planning', 'Development', 'Testing', 'Deployment'],
groupOrder: {
status: ['todo', 'doing', 'done'],
priority: ['critical', 'high', 'medium', 'low'],
phase: ['Planning', 'Development', 'Testing', 'Deployment'],
},
groupStates: {},
};
const groupingSlice = createSlice({
name: 'grouping',
initialState,
reducers: {
setCurrentGrouping: (state, action: PayloadAction<'status' | 'priority' | 'phase'>) => {
state.currentGrouping = action.payload;
},
addCustomPhase: (state, action: PayloadAction<string>) => {
const phase = action.payload.trim();
if (phase && !state.customPhases.includes(phase)) {
state.customPhases.push(phase);
state.groupOrder.phase.push(phase);
}
},
removeCustomPhase: (state, action: PayloadAction<string>) => {
const phase = action.payload;
state.customPhases = state.customPhases.filter(p => p !== phase);
state.groupOrder.phase = state.groupOrder.phase.filter(p => p !== phase);
},
updateCustomPhases: (state, action: PayloadAction<string[]>) => {
state.customPhases = action.payload;
state.groupOrder.phase = action.payload;
},
updateGroupOrder: (state, action: PayloadAction<{ groupType: string; order: string[] }>) => {
const { groupType, order } = action.payload;
state.groupOrder[groupType] = order;
},
toggleGroupCollapsed: (state, action: PayloadAction<string>) => {
const groupId = action.payload;
if (!state.groupStates[groupId]) {
state.groupStates[groupId] = { collapsed: false };
}
state.groupStates[groupId].collapsed = !state.groupStates[groupId].collapsed;
},
setGroupCollapsed: (state, action: PayloadAction<{ groupId: string; collapsed: boolean }>) => {
const { groupId, collapsed } = action.payload;
if (!state.groupStates[groupId]) {
state.groupStates[groupId] = { collapsed: false };
}
state.groupStates[groupId].collapsed = collapsed;
},
collapseAllGroups: (state) => {
Object.keys(state.groupStates).forEach(groupId => {
state.groupStates[groupId].collapsed = true;
});
},
expandAllGroups: (state) => {
Object.keys(state.groupStates).forEach(groupId => {
state.groupStates[groupId].collapsed = false;
});
},
},
});
export const {
setCurrentGrouping,
addCustomPhase,
removeCustomPhase,
updateCustomPhases,
updateGroupOrder,
toggleGroupCollapsed,
setGroupCollapsed,
collapseAllGroups,
expandAllGroups,
} = groupingSlice.actions;
// Selectors
export const selectCurrentGrouping = (state: RootState) => state.grouping.currentGrouping;
export const selectCustomPhases = (state: RootState) => state.grouping.customPhases;
export const selectGroupOrder = (state: RootState) => state.grouping.groupOrder;
export const selectGroupStates = (state: RootState) => state.grouping.groupStates;
// Complex selectors using createSelector for memoization
export const selectCurrentGroupOrder = createSelector(
[selectCurrentGrouping, selectGroupOrder],
(currentGrouping, groupOrder) => groupOrder[currentGrouping] || []
);
export const selectTaskGroups = createSelector(
[taskManagementSelectors.selectAll, selectCurrentGrouping, selectCurrentGroupOrder, selectGroupStates],
(tasks, currentGrouping, groupOrder, groupStates) => {
const groups: TaskGroup[] = [];
// Get unique values for the current grouping
const groupValues = groupOrder.length > 0 ? groupOrder :
[...new Set(tasks.map(task => {
if (currentGrouping === 'status') return task.status;
if (currentGrouping === 'priority') return task.priority;
return task.phase;
}))];
groupValues.forEach(value => {
const tasksInGroup = tasks.filter(task => {
if (currentGrouping === 'status') return task.status === value;
if (currentGrouping === 'priority') return task.priority === value;
return task.phase === value;
}).sort((a, b) => a.order - b.order);
const groupId = `${currentGrouping}-${value}`;
groups.push({
id: groupId,
title: value.charAt(0).toUpperCase() + value.slice(1),
groupType: currentGrouping,
groupValue: value,
collapsed: groupStates[groupId]?.collapsed || false,
taskIds: tasksInGroup.map(task => task.id),
color: getGroupColor(currentGrouping, value),
});
});
return groups;
}
);
export const selectTasksByCurrentGrouping = createSelector(
[taskManagementSelectors.selectAll, selectCurrentGrouping],
(tasks, currentGrouping) => {
const grouped: Record<string, typeof tasks> = {};
tasks.forEach(task => {
let key: string;
if (currentGrouping === 'status') key = task.status;
else if (currentGrouping === 'priority') key = task.priority;
else key = task.phase;
if (!grouped[key]) grouped[key] = [];
grouped[key].push(task);
});
// Sort tasks within each group by order
Object.keys(grouped).forEach(key => {
grouped[key].sort((a, b) => a.order - b.order);
});
return grouped;
}
);
// Helper function to get group colors
const getGroupColor = (groupType: string, value: string): string => {
const colorMaps = {
status: {
todo: '#f0f0f0',
doing: '#1890ff',
done: '#52c41a',
},
priority: {
critical: '#ff4d4f',
high: '#ff7a45',
medium: '#faad14',
low: '#52c41a',
},
phase: {
Planning: '#722ed1',
Development: '#1890ff',
Testing: '#faad14',
Deployment: '#52c41a',
},
};
return colorMaps[groupType as keyof typeof colorMaps]?.[value as keyof any] || '#d9d9d9';
};
export default groupingSlice.reducer;

View File

@@ -0,0 +1,110 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { SelectionState } from '@/types/task-management.types';
import { RootState } from '@/app/store';
const initialState: SelectionState = {
selectedTaskIds: [],
lastSelectedId: null,
};
const selectionSlice = createSlice({
name: 'selection',
initialState,
reducers: {
toggleTaskSelection: (state, action: PayloadAction<string>) => {
const taskId = action.payload;
const index = state.selectedTaskIds.indexOf(taskId);
if (index === -1) {
state.selectedTaskIds.push(taskId);
} else {
state.selectedTaskIds.splice(index, 1);
}
state.lastSelectedId = taskId;
},
selectTask: (state, action: PayloadAction<string>) => {
const taskId = action.payload;
if (!state.selectedTaskIds.includes(taskId)) {
state.selectedTaskIds.push(taskId);
}
state.lastSelectedId = taskId;
},
deselectTask: (state, action: PayloadAction<string>) => {
const taskId = action.payload;
state.selectedTaskIds = state.selectedTaskIds.filter(id => id !== taskId);
if (state.lastSelectedId === taskId) {
state.lastSelectedId = state.selectedTaskIds[state.selectedTaskIds.length - 1] || null;
}
},
selectMultipleTasks: (state, action: PayloadAction<string[]>) => {
const taskIds = action.payload;
// Add new task IDs that aren't already selected
taskIds.forEach(id => {
if (!state.selectedTaskIds.includes(id)) {
state.selectedTaskIds.push(id);
}
});
state.lastSelectedId = taskIds[taskIds.length - 1] || state.lastSelectedId;
},
selectRangeTasks: (state, action: PayloadAction<{ startId: string; endId: string; allTaskIds: string[] }>) => {
const { startId, endId, allTaskIds } = action.payload;
const startIndex = allTaskIds.indexOf(startId);
const endIndex = allTaskIds.indexOf(endId);
if (startIndex !== -1 && endIndex !== -1) {
const [start, end] = startIndex <= endIndex ? [startIndex, endIndex] : [endIndex, startIndex];
const rangeIds = allTaskIds.slice(start, end + 1);
// Add range IDs that aren't already selected
rangeIds.forEach(id => {
if (!state.selectedTaskIds.includes(id)) {
state.selectedTaskIds.push(id);
}
});
state.lastSelectedId = endId;
}
},
selectAllTasks: (state, action: PayloadAction<string[]>) => {
state.selectedTaskIds = action.payload;
state.lastSelectedId = action.payload[action.payload.length - 1] || null;
},
clearSelection: (state) => {
state.selectedTaskIds = [];
state.lastSelectedId = null;
},
setSelection: (state, action: PayloadAction<string[]>) => {
state.selectedTaskIds = action.payload;
state.lastSelectedId = action.payload[action.payload.length - 1] || null;
},
},
});
export const {
toggleTaskSelection,
selectTask,
deselectTask,
selectMultipleTasks,
selectRangeTasks,
selectAllTasks,
clearSelection,
setSelection,
} = selectionSlice.actions;
// Selectors
export const selectSelectedTaskIds = (state: RootState) => state.taskManagementSelection.selectedTaskIds;
export const selectLastSelectedId = (state: RootState) => state.taskManagementSelection.lastSelectedId;
export const selectHasSelection = (state: RootState) => state.taskManagementSelection.selectedTaskIds.length > 0;
export const selectSelectionCount = (state: RootState) => state.taskManagementSelection.selectedTaskIds.length;
export const selectIsTaskSelected = (taskId: string) => (state: RootState) =>
state.taskManagementSelection.selectedTaskIds.includes(taskId);
export default selectionSlice.reducer;

View File

@@ -0,0 +1,135 @@
import { createSlice, createEntityAdapter, PayloadAction } from '@reduxjs/toolkit';
import { Task, TaskManagementState } from '@/types/task-management.types';
import { RootState } from '@/app/store';
// Entity adapter for normalized state
const tasksAdapter = createEntityAdapter<Task>({
selectId: (task) => task.id,
sortComparer: (a, b) => a.order - b.order,
});
const initialState: TaskManagementState = {
entities: {},
ids: [],
loading: false,
error: null,
};
const taskManagementSlice = createSlice({
name: 'taskManagement',
initialState: tasksAdapter.getInitialState(initialState),
reducers: {
// Basic CRUD operations
setTasks: (state, action: PayloadAction<Task[]>) => {
tasksAdapter.setAll(state, action.payload);
state.loading = false;
state.error = null;
},
addTask: (state, action: PayloadAction<Task>) => {
tasksAdapter.addOne(state, action.payload);
},
updateTask: (state, action: PayloadAction<{ id: string; changes: Partial<Task> }>) => {
tasksAdapter.updateOne(state, {
id: action.payload.id,
changes: {
...action.payload.changes,
updatedAt: new Date().toISOString(),
},
});
},
deleteTask: (state, action: PayloadAction<string>) => {
tasksAdapter.removeOne(state, action.payload);
},
// Bulk operations
bulkUpdateTasks: (state, action: PayloadAction<{ ids: string[]; changes: Partial<Task> }>) => {
const { ids, changes } = action.payload;
const updates = ids.map(id => ({
id,
changes: {
...changes,
updatedAt: new Date().toISOString(),
},
}));
tasksAdapter.updateMany(state, updates);
},
bulkDeleteTasks: (state, action: PayloadAction<string[]>) => {
tasksAdapter.removeMany(state, action.payload);
},
// Drag and drop operations
reorderTasks: (state, action: PayloadAction<{ taskIds: string[]; newOrder: number[] }>) => {
const { taskIds, newOrder } = action.payload;
const updates = taskIds.map((id, index) => ({
id,
changes: { order: newOrder[index] },
}));
tasksAdapter.updateMany(state, updates);
},
moveTaskToGroup: (state, action: PayloadAction<{ taskId: string; groupType: 'status' | 'priority' | 'phase'; groupValue: string }>) => {
const { taskId, groupType, groupValue } = action.payload;
const changes: Partial<Task> = {
updatedAt: new Date().toISOString(),
};
// Update the appropriate field based on group type
if (groupType === 'status') {
changes.status = groupValue as Task['status'];
} else if (groupType === 'priority') {
changes.priority = groupValue as Task['priority'];
} else if (groupType === 'phase') {
changes.phase = groupValue;
}
tasksAdapter.updateOne(state, { id: taskId, changes });
},
// Loading states
setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload;
},
setError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload;
state.loading = false;
},
},
});
export const {
setTasks,
addTask,
updateTask,
deleteTask,
bulkUpdateTasks,
bulkDeleteTasks,
reorderTasks,
moveTaskToGroup,
setLoading,
setError,
} = taskManagementSlice.actions;
// Selectors
export const taskManagementSelectors = tasksAdapter.getSelectors<RootState>(
(state) => state.taskManagement
);
// Additional selectors
export const selectTasksByStatus = (state: RootState, status: string) =>
taskManagementSelectors.selectAll(state).filter(task => task.status === status);
export const selectTasksByPriority = (state: RootState, priority: string) =>
taskManagementSelectors.selectAll(state).filter(task => task.priority === priority);
export const selectTasksByPhase = (state: RootState, phase: string) =>
taskManagementSelectors.selectAll(state).filter(task => task.phase === phase);
export const selectTasksLoading = (state: RootState) => state.taskManagement.loading;
export const selectTasksError = (state: RootState) => state.taskManagement.error;
export default taskManagementSlice.reducer;

View File

@@ -5,6 +5,7 @@ import ProjectViewMembers from '@/pages/projects/projectView/members/project-vie
import ProjectViewUpdates from '@/pages/projects/project-view-1/updates/project-view-updates';
import ProjectViewTaskList from '@/pages/projects/projectView/taskList/project-view-task-list';
import ProjectViewBoard from '@/pages/projects/projectView/board/project-view-board';
import ProjectViewEnhancedTasks from '@/pages/projects/projectView/enhancedTasks/project-view-enhanced-tasks';
// type of a tab items
type TabItems = {
@@ -22,47 +23,54 @@ export const tabItems: TabItems[] = [
key: 'tasks-list',
label: 'Task List',
isPinned: true,
element: React.createElement(ProjectViewTaskList),
element: React.createElement(ProjectViewEnhancedTasks),
},
{
index: 1,
key: 'task-list-v1',
label: 'Task List v1',
isPinned: true,
element: React.createElement(ProjectViewTaskList),
},
{
index: 2,
key: 'board',
label: 'Board',
isPinned: true,
element: React.createElement(ProjectViewBoard),
},
// {
// index: 2,
// index: 3,
// key: 'workload',
// label: 'Workload',
// element: React.createElement(ProjectViewWorkload),
// },
// {
// index: 3,
// index: 4,
// key: 'roadmap',
// label: 'Roadmap',
// element: React.createElement(ProjectViewRoadmap),
// },
{
index: 4,
index: 5,
key: 'project-insights-member-overview',
label: 'Insights',
element: React.createElement(ProjectViewInsights),
},
{
index: 5,
index: 6,
key: 'all-attachments',
label: 'Files',
element: React.createElement(ProjectViewFiles),
},
{
index: 6,
index: 7,
key: 'members',
label: 'Members',
element: React.createElement(ProjectViewMembers),
},
{
index: 7,
index: 8,
key: 'updates',
label: 'Updates',
element: React.createElement(ProjectViewUpdates),

View File

@@ -0,0 +1,78 @@
import React, { useEffect } from 'react';
import { Layout, Typography, Card, Space, Alert } from 'antd';
import { useDispatch } from 'react-redux';
import TaskListBoard from '@/components/task-management/TaskListBoard';
import { AppDispatch } from '@/app/store';
const { Header, Content } = Layout;
const { Title, Paragraph } = Typography;
const TaskManagementDemo: React.FC = () => {
const dispatch = useDispatch<AppDispatch>();
// Mock project ID for demo
const demoProjectId = 'demo-project-123';
useEffect(() => {
// Initialize demo data if needed
// You might want to populate some sample tasks here for demonstration
}, [dispatch]);
return (
<Layout className="min-h-screen bg-gray-50">
<Header className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4">
<Title level={2} className="mb-0 text-gray-800">
Enhanced Task Management System
</Title>
</div>
</Header>
<Content className="max-w-7xl mx-auto px-4 py-6 w-full">
<Space direction="vertical" size="large" className="w-full">
{/* Introduction */}
<Card>
<Title level={3}>Task Management Features</Title>
<Paragraph>
This enhanced task management system provides a comprehensive interface for managing tasks
with the following key features:
</Paragraph>
<ul className="list-disc list-inside space-y-1 text-gray-700">
<li><strong>Dynamic Grouping:</strong> Group tasks by Status, Priority, or Phase</li>
<li><strong>Drag & Drop:</strong> Reorder tasks within groups or move between groups</li>
<li><strong>Multi-select:</strong> Select multiple tasks for bulk operations</li>
<li><strong>Bulk Actions:</strong> Change status, priority, assignees, or delete multiple tasks</li>
<li><strong>Subtasks:</strong> Expandable subtask support with progress tracking</li>
<li><strong>Real-time Updates:</strong> Live updates via WebSocket connections</li>
<li><strong>Rich Task Display:</strong> Progress bars, assignees, labels, due dates, and more</li>
</ul>
</Card>
{/* Usage Instructions */}
<Alert
message="Demo Instructions"
description={
<div>
<p><strong>Grouping:</strong> Use the dropdown to switch between Status, Priority, and Phase grouping.</p>
<p><strong>Drag & Drop:</strong> Click and drag tasks to reorder within groups or move between groups.</p>
<p><strong>Selection:</strong> Click checkboxes to select tasks, then use bulk actions in the blue bar.</p>
<p><strong>Subtasks:</strong> Click the +/- buttons next to task names to expand/collapse subtasks.</p>
</div>
}
type="info"
showIcon
className="mb-4"
/>
{/* Task List Board */}
<TaskListBoard
projectId={demoProjectId}
className="task-management-demo"
/>
</Space>
</Content>
</Layout>
);
};
export default TaskManagementDemo;

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { useAppSelector } from '@/hooks/useAppSelector';
import TaskListBoard from '@/components/task-management/TaskListBoard';
const ProjectViewEnhancedTasks: React.FC = () => {
const { project } = useAppSelector(state => state.projectReducer);
if (!project?.id) {
return (
<div className="p-4 text-center text-gray-500">
Project not found
</div>
);
}
return (
<div className="project-view-enhanced-tasks">
<TaskListBoard projectId={project.id} />
</div>
);
};
export default ProjectViewEnhancedTasks;

View File

@@ -1,5 +1,7 @@
import Input, { InputRef } from 'antd/es/input';
import { useMemo, useRef, useState } from 'react';
import { useMemo, useRef, useState, useEffect } from 'react';
import { Spin } from 'antd';
import { LoadingOutlined } from '@ant-design/icons';
import { useAppSelector } from '@/hooks/useAppSelector';
import { colors } from '@/styles/colors';
import { useTranslation } from 'react-i18next';
@@ -31,7 +33,10 @@ const AddTaskListRow = ({ groupId = null, parentTask = null }: IAddTaskListRowPr
const [isEdit, setIsEdit] = useState<boolean>(false);
const [taskName, setTaskName] = useState<string>('');
const [creatingTask, setCreatingTask] = useState<boolean>(false);
const [error, setError] = useState<string>('');
const [taskCreationTimeout, setTaskCreationTimeout] = useState<NodeJS.Timeout | null>(null);
const taskInputRef = useRef<InputRef>(null);
const containerRef = useRef<HTMLDivElement>(null);
const dispatch = useAppDispatch();
const currentSession = useAuthService().getCurrentSession();
@@ -43,13 +48,62 @@ const AddTaskListRow = ({ groupId = null, parentTask = null }: IAddTaskListRowPr
const customBorderColor = useMemo(() => themeMode === 'dark' && ' border-[#303030]', [themeMode]);
const projectId = useAppSelector(state => state.projectReducer.projectId);
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (taskCreationTimeout) {
clearTimeout(taskCreationTimeout);
}
};
}, [taskCreationTimeout]);
// Handle click outside to cancel edit mode
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
isEdit &&
!creatingTask &&
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
cancelEdit();
}
};
if (isEdit) {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}
}, [isEdit, creatingTask]);
const createRequestBody = (): ITaskCreateRequest | null => {
if (!projectId || !currentSession) return null;
const body: ITaskCreateRequest = {
project_id: projectId,
id: '',
name: taskName,
reporter_id: currentSession.id,
description: '',
status_id: '',
priority: '',
start_date: '',
end_date: '',
total_hours: 0,
total_minutes: 0,
billable: false,
phase_id: '',
parent_task_id: undefined,
project_id: projectId,
team_id: currentSession.team_id,
task_key: '',
labels: [],
assignees: [],
names: [],
sub_tasks_count: 0,
manual_progress: false,
progress_value: null,
weight: null,
reporter_id: currentSession.id,
};
const groupBy = getCurrentGroup();
@@ -69,10 +123,14 @@ const AddTaskListRow = ({ groupId = null, parentTask = null }: IAddTaskListRowPr
const reset = (scroll = true) => {
setIsEdit(false);
setCreatingTask(false);
setTaskName('');
setError('');
if (taskCreationTimeout) {
clearTimeout(taskCreationTimeout);
setTaskCreationTimeout(null);
}
setIsEdit(true);
setTimeout(() => {
@@ -81,6 +139,16 @@ const AddTaskListRow = ({ groupId = null, parentTask = null }: IAddTaskListRowPr
}, DRAWER_ANIMATION_INTERVAL);
};
const cancelEdit = () => {
setIsEdit(false);
setTaskName('');
setError('');
if (taskCreationTimeout) {
clearTimeout(taskCreationTimeout);
setTaskCreationTimeout(null);
}
};
const onNewTaskReceived = (task: IAddNewTask) => {
if (!groupId) return;
@@ -106,49 +174,210 @@ const AddTaskListRow = ({ groupId = null, parentTask = null }: IAddTaskListRowPr
};
const addInstantTask = async () => {
if (creatingTask || !projectId || !currentSession || taskName.trim() === '') return;
// Validation
if (creatingTask || !projectId || !currentSession) return;
const trimmedTaskName = taskName.trim();
if (trimmedTaskName === '') {
setError('Task name cannot be empty');
taskInputRef.current?.focus();
return;
}
try {
setCreatingTask(true);
setError('');
const body = createRequestBody();
if (!body) return;
if (!body) {
setError('Failed to create task. Please try again.');
setCreatingTask(false);
return;
}
// Set timeout for task creation (10 seconds)
const timeout = setTimeout(() => {
setCreatingTask(false);
setError('Task creation timed out. Please try again.');
}, 10000);
setTaskCreationTimeout(timeout);
socket?.emit(SocketEvents.QUICK_TASK.toString(), JSON.stringify(body));
// Handle success response
socket?.once(SocketEvents.QUICK_TASK.toString(), (task: IProjectTask) => {
clearTimeout(timeout);
setTaskCreationTimeout(null);
setCreatingTask(false);
onNewTaskReceived(task as IAddNewTask);
if (task && task.id) {
onNewTaskReceived(task as IAddNewTask);
} else {
setError('Failed to create task. Please try again.');
}
});
// Handle error response
socket?.once('error', (errorData: any) => {
clearTimeout(timeout);
setTaskCreationTimeout(null);
setCreatingTask(false);
const errorMessage = errorData?.message || 'Failed to create task';
setError(errorMessage);
});
} catch (error) {
console.error('Error adding task:', error);
setCreatingTask(false);
setError('An unexpected error occurred. Please try again.');
}
};
const handleAddTask = () => {
setIsEdit(false);
if (creatingTask) return; // Prevent multiple submissions
addInstantTask();
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Escape') {
e.preventDefault();
cancelEdit();
} else if (e.key === 'Enter' && !creatingTask) {
e.preventDefault();
handleAddTask();
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setTaskName(e.target.value);
if (error) setError(''); // Clear error when user starts typing
};
return (
<div>
<div className="add-task-row-container" ref={containerRef}>
{isEdit ? (
<Input
className="h-12 w-full rounded-none"
style={{ borderColor: colors.skyBlue }}
placeholder={t('addTaskInputPlaceholder')}
onChange={e => setTaskName(e.target.value)}
onBlur={handleAddTask}
onPressEnter={handleAddTask}
ref={taskInputRef}
/>
<div className="add-task-input-container">
<Input
className="add-task-input"
style={{
borderColor: error ? '#ff4d4f' : colors.skyBlue,
paddingRight: creatingTask ? '32px' : '12px'
}}
placeholder={t('addTaskInputPlaceholder')}
value={taskName}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
ref={taskInputRef}
autoFocus
disabled={creatingTask}
/>
{creatingTask && (
<div className="add-task-loading">
<Spin
size="small"
indicator={<LoadingOutlined style={{ fontSize: 14 }} spin />}
/>
</div>
)}
{error && (
<div className="add-task-error">
{error}
</div>
)}
</div>
) : (
<Input
onFocus={() => setIsEdit(true)}
className="w-[300px] border-none"
value={parentTask ? t('addSubTaskText') : t('addTaskText')}
ref={taskInputRef}
/>
<div
className="add-task-label"
onClick={() => setIsEdit(true)}
>
<span className="add-task-text">
{parentTask ? t('addSubTaskText') : t('addTaskText')}
</span>
</div>
)}
<style>{`
.add-task-row-container {
width: 100%;
transition: height 0.3s ease;
}
.add-task-input-container {
position: relative;
width: 100%;
}
.add-task-input {
width: 100%;
height: 40px;
border-radius: 6px;
border: 1px solid ${colors.skyBlue};
font-size: 14px;
padding: 0 12px;
margin: 2px 0;
transition: border-color 0.2s ease, background-color 0.2s ease;
}
.add-task-input:disabled {
background-color: var(--task-bg-secondary, #f5f5f5);
cursor: not-allowed;
opacity: 0.7;
}
.add-task-loading {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
z-index: 1;
}
.add-task-error {
font-size: 12px;
color: #ff4d4f;
margin-top: 4px;
margin-left: 2px;
line-height: 1.4;
}
.add-task-label {
width: 100%;
height: 40px;
display: flex;
align-items: center;
padding: 0;
cursor: pointer;
border-radius: 6px;
border: 1px solid transparent;
transition: all 0.2s ease;
color: var(--task-text-tertiary, #8c8c8c);
}
.add-task-label:hover {
background: var(--task-hover-bg, #fafafa);
border-color: var(--task-border-tertiary, #d9d9d9);
color: var(--task-text-secondary, #595959);
}
.add-task-text {
font-size: 14px;
user-select: none;
}
/* Dark mode support */
.dark .add-task-label,
[data-theme="dark"] .add-task-label {
color: var(--task-text-tertiary, #8c8c8c);
}
.dark .add-task-label:hover,
[data-theme="dark"] .add-task-label:hover {
background: var(--task-hover-bg, #2a2a2a);
border-color: var(--task-border-tertiary, #505050);
color: var(--task-text-secondary, #d9d9d9);
}
`}</style>
</div>
);
};

View File

@@ -0,0 +1,601 @@
/* Task Management System Styles */
.task-list-board {
width: 100%;
}
.task-group {
transition: all 0.2s ease;
}
.task-group.drag-over {
border-color: #1890ff !important;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.task-group .group-header {
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
}
.task-group .group-header:hover {
background: #f5f5f5;
}
.task-row {
border-left: 2px solid transparent;
transition: all 0.2s ease;
}
.task-row:hover {
background-color: #f9f9f9 !important;
border-left-color: #d9d9d9;
}
.task-row.selected {
background-color: #e6f7ff !important;
border-left-color: #1890ff;
}
.task-row .drag-handle {
cursor: grab;
transition: opacity 0.2s ease;
}
.task-row .drag-handle:active {
cursor: grabbing;
}
.task-row:not(:hover) .drag-handle {
opacity: 0.3;
}
/* Progress bars */
.ant-progress-line {
margin: 0;
}
.ant-progress-bg {
border-radius: 2px;
}
/* Avatar groups */
.ant-avatar-group .ant-avatar {
border: 1px solid #fff;
}
/* Tags */
.task-row .ant-tag {
margin: 0;
padding: 0 4px;
height: 16px;
line-height: 14px;
font-size: 10px;
border-radius: 2px;
}
/* Checkboxes */
.task-row .ant-checkbox-wrapper {
margin-right: 0;
}
/* Bulk action bar */
.bulk-action-bar {
position: sticky;
top: 0;
z-index: 10;
background: #e6f7ff;
border: 1px solid #91d5ff;
border-radius: 6px;
margin-bottom: 16px;
}
/* Collapsible animations */
.task-group .ant-collapse-content > .ant-collapse-content-box {
padding: 0;
}
/* Drag overlay */
.task-row.drag-overlay {
background: white;
border: 1px solid #d9d9d9;
border-radius: 6px;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
transform: rotate(5deg);
cursor: grabbing;
z-index: 1000;
}
/* Responsive design */
@media (max-width: 768px) {
.task-row {
padding: 12px;
}
.task-row .flex {
flex-direction: column;
align-items: flex-start;
}
.task-row .task-metadata {
margin-top: 8px;
margin-left: 0 !important;
}
}
/* Subtask indentation */
.task-subtasks {
margin-left: 32px;
padding-left: 16px;
border-left: 2px solid #f0f0f0;
}
.task-subtasks .task-row {
padding: 8px 16px;
font-size: 13px;
}
/* Status indicators */
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
}
.priority-indicator {
width: 6px;
height: 6px;
border-radius: 50%;
display: inline-block;
}
/* Animation classes */
.task-row-enter {
opacity: 0;
transform: translateY(-10px);
}
.task-row-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 300ms, transform 300ms;
}
.task-row-exit {
opacity: 1;
}
.task-row-exit-active {
opacity: 0;
transform: translateY(-10px);
transition: opacity 300ms, transform 300ms;
}
/* Custom scrollbar */
.task-groups-container::-webkit-scrollbar {
width: 6px;
}
.task-groups-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.task-groups-container::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.task-groups-container::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* Loading states */
.task-row.loading {
opacity: 0.6;
pointer-events: none;
}
.task-group.loading {
opacity: 0.8;
}
/* Focus styles for accessibility */
.task-row:focus-within {
outline: 2px solid #1890ff;
outline-offset: 2px;
}
.drag-handle:focus {
outline: 2px solid #1890ff;
outline-offset: 2px;
}
/* Dark mode support */
[data-theme="dark"] .task-list-board {
background-color: #141414;
color: rgba(255, 255, 255, 0.85);
}
@media (prefers-color-scheme: dark) {
.task-list-board {
background-color: #141414;
color: rgba(255, 255, 255, 0.85);
}
/* Task Groups */
.task-group {
background-color: #1f1f1f;
border-color: #303030;
}
.task-group.drag-over {
border-color: #1890ff !important;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.3);
background-color: rgba(24, 144, 255, 0.1);
}
.task-group .group-header {
background: #262626;
border-bottom-color: #303030;
color: rgba(255, 255, 255, 0.85);
}
.task-group .group-header:hover {
background: #2f2f2f;
}
/* Task Rows */
.task-row {
background-color: #1f1f1f;
color: rgba(255, 255, 255, 0.85);
border-color: #303030;
}
.task-row:hover {
background-color: #262626 !important;
border-left-color: #595959;
}
.task-row.selected {
background-color: rgba(24, 144, 255, 0.15) !important;
border-left-color: #1890ff;
}
.task-row .drag-handle {
color: rgba(255, 255, 255, 0.45);
}
.task-row .drag-handle:hover {
color: rgba(255, 255, 255, 0.85);
}
/* Progress bars */
.ant-progress-bg {
background-color: #303030;
}
/* Text colors */
.task-row .ant-typography {
color: rgba(255, 255, 255, 0.85);
}
.task-row .text-gray-500 {
color: rgba(255, 255, 255, 0.45) !important;
}
.task-row .text-gray-600 {
color: rgba(255, 255, 255, 0.65) !important;
}
.task-row .text-gray-400 {
color: rgba(255, 255, 255, 0.45) !important;
}
/* Completed task styling */
.task-row .line-through {
color: rgba(255, 255, 255, 0.45);
}
/* Bulk Action Bar */
.bulk-action-bar {
background: rgba(24, 144, 255, 0.15);
border-color: rgba(24, 144, 255, 0.3);
color: rgba(255, 255, 255, 0.85);
}
/* Cards and containers */
.ant-card {
background-color: #1f1f1f;
border-color: #303030;
color: rgba(255, 255, 255, 0.85);
}
.ant-card-head {
background-color: #262626;
border-bottom-color: #303030;
color: rgba(255, 255, 255, 0.85);
}
.ant-card-body {
background-color: #1f1f1f;
color: rgba(255, 255, 255, 0.85);
}
/* Buttons */
.ant-btn {
border-color: #303030;
color: rgba(255, 255, 255, 0.85);
}
.ant-btn:hover {
border-color: #595959;
color: rgba(255, 255, 255, 0.85);
}
.ant-btn-primary {
background-color: #1890ff;
border-color: #1890ff;
}
.ant-btn-primary:hover {
background-color: #40a9ff;
border-color: #40a9ff;
}
/* Dropdowns and menus */
.ant-dropdown-menu {
background-color: #1f1f1f;
border-color: #303030;
}
.ant-dropdown-menu-item {
color: rgba(255, 255, 255, 0.85);
}
.ant-dropdown-menu-item:hover {
background-color: #262626;
}
/* Select components */
.ant-select-selector {
background-color: #1f1f1f !important;
border-color: #303030 !important;
color: rgba(255, 255, 255, 0.85) !important;
}
.ant-select-arrow {
color: rgba(255, 255, 255, 0.45);
}
/* Checkboxes */
.ant-checkbox-wrapper {
color: rgba(255, 255, 255, 0.85);
}
.ant-checkbox-inner {
background-color: #1f1f1f;
border-color: #303030;
}
.ant-checkbox-checked .ant-checkbox-inner {
background-color: #1890ff;
border-color: #1890ff;
}
/* Tags and labels */
.ant-tag {
background-color: #262626;
border-color: #303030;
color: rgba(255, 255, 255, 0.85);
}
/* Avatars */
.ant-avatar {
background-color: #595959;
color: rgba(255, 255, 255, 0.85);
}
/* Tooltips */
.ant-tooltip-inner {
background-color: #262626;
color: rgba(255, 255, 255, 0.85);
}
.ant-tooltip-arrow-content {
background-color: #262626;
}
/* Popconfirm */
.ant-popover-inner {
background-color: #1f1f1f;
color: rgba(255, 255, 255, 0.85);
}
.ant-popover-arrow-content {
background-color: #1f1f1f;
}
/* Subtasks */
.task-subtasks {
border-left-color: #303030;
}
.task-subtasks .task-row {
background-color: #141414;
}
.task-subtasks .task-row:hover {
background-color: #1f1f1f !important;
}
/* Scrollbars */
.task-groups-container::-webkit-scrollbar-track {
background: #141414;
}
.task-groups-container::-webkit-scrollbar-thumb {
background: #595959;
}
.task-groups-container::-webkit-scrollbar-thumb:hover {
background: #777777;
}
/* Loading states */
.ant-spin-dot-item {
background-color: #1890ff;
}
/* Empty states */
.ant-empty {
color: rgba(255, 255, 255, 0.45);
}
.ant-empty-description {
color: rgba(255, 255, 255, 0.45);
}
/* Focus styles for dark mode */
.task-row:focus-within {
outline-color: #40a9ff;
}
.drag-handle:focus {
outline-color: #40a9ff;
}
/* Border colors */
.border-gray-100 {
border-color: #303030 !important;
}
.border-gray-200 {
border-color: #404040 !important;
}
.border-gray-300 {
border-color: #595959 !important;
}
/* Background utilities */
.bg-gray-50 {
background-color: #141414 !important;
}
.bg-gray-100 {
background-color: #1f1f1f !important;
}
.bg-white {
background-color: #1f1f1f !important;
}
/* Due date colors in dark mode */
.text-red-500 {
color: #ff7875 !important;
}
.text-orange-500 {
color: #ffa940 !important;
}
/* Group progress bar in dark mode */
.task-group .group-header .bg-gray-200 {
background-color: #303030 !important;
}
}
/* Specific dark mode styles using data-theme attribute */
[data-theme="dark"] .task-group {
background-color: #1f1f1f;
border-color: #303030;
}
[data-theme="dark"] .task-group.drag-over {
border-color: #1890ff !important;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.3);
background-color: rgba(24, 144, 255, 0.1);
}
[data-theme="dark"] .task-group .group-header {
background: #262626;
border-bottom-color: #303030;
color: rgba(255, 255, 255, 0.85);
}
[data-theme="dark"] .task-group .group-header:hover {
background: #2f2f2f;
}
[data-theme="dark"] .task-row {
background-color: #1f1f1f;
color: rgba(255, 255, 255, 0.85);
border-color: #303030;
}
[data-theme="dark"] .task-row:hover {
background-color: #262626 !important;
border-left-color: #595959;
}
[data-theme="dark"] .task-row.selected {
background-color: rgba(24, 144, 255, 0.15) !important;
border-left-color: #1890ff;
}
[data-theme="dark"] .task-row .drag-handle {
color: rgba(255, 255, 255, 0.45);
}
[data-theme="dark"] .task-row .drag-handle:hover {
color: rgba(255, 255, 255, 0.85);
}
[data-theme="dark"] .bulk-action-bar {
background: rgba(24, 144, 255, 0.15);
border-color: rgba(24, 144, 255, 0.3);
color: rgba(255, 255, 255, 0.85);
}
[data-theme="dark"] .task-row .ant-typography {
color: rgba(255, 255, 255, 0.85);
}
[data-theme="dark"] .task-row .text-gray-500 {
color: rgba(255, 255, 255, 0.45) !important;
}
[data-theme="dark"] .task-row .text-gray-600 {
color: rgba(255, 255, 255, 0.65) !important;
}
[data-theme="dark"] .task-row .text-gray-400 {
color: rgba(255, 255, 255, 0.45) !important;
}
[data-theme="dark"] .task-row .line-through {
color: rgba(255, 255, 255, 0.45);
}
[data-theme="dark"] .task-subtasks {
border-left-color: #303030;
}
[data-theme="dark"] .task-subtasks .task-row {
background-color: #141414;
}
[data-theme="dark"] .task-subtasks .task-row:hover {
background-color: #1f1f1f !important;
}
[data-theme="dark"] .text-red-500 {
color: #ff7875 !important;
}
[data-theme="dark"] .text-orange-500 {
color: #ffa940 !important;
}

View File

@@ -0,0 +1,125 @@
export interface Task {
id: string;
title: string;
description?: string;
status: 'todo' | 'doing' | 'done';
priority: 'critical' | 'high' | 'medium' | 'low';
phase: string; // Custom phases like 'planning', 'development', 'testing', 'deployment'
progress: number; // 0-100
assignees: string[];
labels: string[];
dueDate?: string;
timeTracking: {
estimated?: number;
logged: number;
};
customFields: Record<string, any>;
createdAt: string;
updatedAt: string;
order: number;
}
export interface TaskGroup {
id: string;
title: string;
groupType: 'status' | 'priority' | 'phase';
groupValue: string; // The actual value for the group (e.g., 'todo', 'high', 'development')
collapsed: boolean;
taskIds: string[];
color?: string; // For visual distinction
}
export interface GroupingConfig {
currentGrouping: 'status' | 'priority' | 'phase';
customPhases: string[]; // User-defined phases
groupOrder: Record<string, string[]>; // Order of groups for each grouping type
}
export interface Column {
id: string;
title: string;
dataIndex: string;
width: number;
visible: boolean;
editable: boolean;
type: 'text' | 'select' | 'date' | 'progress' | 'tags' | 'users';
}
export interface User {
id: string;
name: string;
email: string;
avatar?: string;
}
export interface Label {
id: string;
name: string;
color: string;
}
// Redux State Interfaces
export interface TaskManagementState {
entities: Record<string, Task>;
ids: string[];
loading: boolean;
error: string | null;
}
export interface TaskGroupsState {
entities: Record<string, TaskGroup>;
ids: string[];
}
export interface GroupingState {
currentGrouping: 'status' | 'priority' | 'phase';
customPhases: string[];
groupOrder: Record<string, string[]>;
groupStates: Record<string, { collapsed: boolean }>; // Persist group states
}
export interface SelectionState {
selectedTaskIds: string[];
lastSelectedId: string | null;
}
export interface ColumnsState {
entities: Record<string, Column>;
ids: string[];
order: string[];
}
export interface UIState {
draggedTaskId: string | null;
bulkActionMode: boolean;
editingCell: { taskId: string; field: string } | null;
}
// Drag and Drop
export interface DragEndEvent {
active: {
id: string;
data: {
current?: {
taskId: string;
groupId: string;
};
};
};
over: {
id: string;
data: {
current?: {
groupId: string;
type: 'group' | 'task';
};
};
} | null;
}
// Bulk Actions
export interface BulkAction {
type: 'status' | 'priority' | 'phase' | 'assignee' | 'label' | 'delete';
value?: any;
taskIds: string[];
}

View File

@@ -0,0 +1,166 @@
import { Task, User, Label } from '@/types/task-management.types';
import { nanoid } from 'nanoid';
// Mock users
export const mockUsers: User[] = [
{ id: '1', name: 'John Doe', email: 'john@example.com', avatar: '' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com', avatar: '' },
{ id: '3', name: 'Bob Johnson', email: 'bob@example.com', avatar: '' },
{ id: '4', name: 'Alice Brown', email: 'alice@example.com', avatar: '' },
{ id: '5', name: 'Charlie Wilson', email: 'charlie@example.com', avatar: '' },
];
// Mock labels
export const mockLabels: Label[] = [
{ id: '1', name: 'Bug', color: '#ff4d4f' },
{ id: '2', name: 'Feature', color: '#52c41a' },
{ id: '3', name: 'Enhancement', color: '#1890ff' },
{ id: '4', name: 'Documentation', color: '#722ed1' },
{ id: '5', name: 'Urgent', color: '#fa541c' },
{ id: '6', name: 'Research', color: '#faad14' },
];
// Task titles for variety
const taskTitles = [
'Implement user authentication system',
'Design responsive navigation component',
'Fix CSS styling issues on mobile',
'Add drag and drop functionality',
'Optimize database queries',
'Write unit tests for API endpoints',
'Update documentation for new features',
'Refactor legacy code components',
'Set up CI/CD pipeline',
'Configure monitoring and logging',
'Implement real-time notifications',
'Create user onboarding flow',
'Add search functionality',
'Optimize image loading performance',
'Implement data export feature',
'Add multi-language support',
'Create admin dashboard',
'Fix memory leak in background process',
'Implement caching strategy',
'Add email notification system',
'Create API rate limiting',
'Implement user roles and permissions',
'Add file upload functionality',
'Create backup and restore system',
'Implement advanced filtering',
'Add calendar integration',
'Create reporting dashboard',
'Implement websocket connections',
'Add payment processing',
'Create mobile app version',
];
const taskDescriptions = [
'This task requires careful consideration of security best practices and user experience.',
'Need to ensure compatibility across all modern browsers and devices.',
'Critical bug that affects user workflow and needs immediate attention.',
'Enhancement to improve overall system performance and user satisfaction.',
'Research task to explore new technologies and implementation approaches.',
'Documentation update to keep project information current and accurate.',
'Refactoring work to improve code maintainability and reduce technical debt.',
'Testing task to ensure reliability and prevent regression bugs.',
];
const statuses: Task['status'][] = ['todo', 'doing', 'done'];
const priorities: Task['priority'][] = ['critical', 'high', 'medium', 'low'];
const phases = ['Planning', 'Development', 'Testing', 'Deployment'];
function getRandomElement<T>(array: T[]): T {
return array[Math.floor(Math.random() * array.length)];
}
function getRandomElements<T>(array: T[], min: number = 0, max?: number): T[] {
const maxCount = max ?? array.length;
const count = Math.floor(Math.random() * (maxCount - min + 1)) + min;
const shuffled = [...array].sort(() => 0.5 - Math.random());
return shuffled.slice(0, count);
}
function getRandomProgress(): number {
const progressOptions = [0, 10, 25, 50, 75, 90, 100];
return getRandomElement(progressOptions);
}
function getRandomTimeTracking() {
const estimated = Math.floor(Math.random() * 40) + 1; // 1-40 hours
const logged = Math.floor(Math.random() * estimated); // 0 to estimated hours
return { estimated, logged };
}
function getRandomDueDate(): string | undefined {
if (Math.random() < 0.7) { // 70% chance of having a due date
const now = new Date();
const daysToAdd = Math.floor(Math.random() * 30) - 10; // -10 to +20 days from now
const dueDate = new Date(now.getTime() + daysToAdd * 24 * 60 * 60 * 1000);
return dueDate.toISOString().split('T')[0];
}
return undefined;
}
export function generateMockTask(index: number): Task {
const now = new Date();
const createdAt = new Date(now.getTime() - Math.random() * 30 * 24 * 60 * 60 * 1000); // Up to 30 days ago
return {
id: nanoid(),
title: getRandomElement(taskTitles),
description: Math.random() < 0.8 ? getRandomElement(taskDescriptions) : undefined,
status: getRandomElement(statuses),
priority: getRandomElement(priorities),
phase: getRandomElement(phases),
progress: getRandomProgress(),
assignees: getRandomElements(mockUsers, 0, 3).map(user => user.id), // 0-3 assignees
labels: getRandomElements(mockLabels, 0, 4).map(label => label.id), // 0-4 labels
dueDate: getRandomDueDate(),
timeTracking: getRandomTimeTracking(),
customFields: {},
createdAt: createdAt.toISOString(),
updatedAt: createdAt.toISOString(),
order: index,
};
}
export function generateMockTasks(count: number = 100): Task[] {
return Array.from({ length: count }, (_, index) => generateMockTask(index));
}
// Generate tasks with specific distribution for testing
export function generateBalancedMockTasks(count: number = 100): Task[] {
const tasks: Task[] = [];
const statusDistribution = { todo: 0.4, doing: 0.4, done: 0.2 };
const priorityDistribution = { critical: 0.1, high: 0.3, medium: 0.4, low: 0.2 };
for (let i = 0; i < count; i++) {
const task = generateMockTask(i);
// Distribute statuses
const statusRand = Math.random();
if (statusRand < statusDistribution.todo) {
task.status = 'todo';
} else if (statusRand < statusDistribution.todo + statusDistribution.doing) {
task.status = 'doing';
} else {
task.status = 'done';
}
// Distribute priorities
const priorityRand = Math.random();
if (priorityRand < priorityDistribution.critical) {
task.priority = 'critical';
} else if (priorityRand < priorityDistribution.critical + priorityDistribution.high) {
task.priority = 'high';
} else if (priorityRand < priorityDistribution.critical + priorityDistribution.high + priorityDistribution.medium) {
task.priority = 'medium';
} else {
task.priority = 'low';
}
tasks.push(task);
}
return tasks;
}