feat(task-management): enhance task list with drag-and-drop functionality and improved styling

- Integrated drag-and-drop capabilities using @dnd-kit for task reordering within the TaskListV2 component.
- Updated TaskGroupHeader to dynamically adjust colors based on group properties, improving visual clarity.
- Refactored TaskRow to support drag-and-drop interactions and optimized rendering of task details.
- Enhanced Redux state management for collapsed groups, transitioning from Set to array for better compatibility.
- Improved task display logic to handle optional fields and ensure consistent rendering across components.
This commit is contained in:
chamikaJ
2025-07-03 17:34:31 +05:30
parent d15c00c29b
commit edf051adc7
7 changed files with 437 additions and 187 deletions

View File

@@ -1,10 +1,12 @@
import { createSlice, PayloadAction, createSelector } from '@reduxjs/toolkit';
import { GroupingState, TaskGroup, TaskGrouping } from '@/types/task-management.types';
import { TaskGroup } from '@/types/task-management.types';
import { RootState } from '@/app/store';
import { selectAllTasks } from './task-management.slice';
import { selectAllTasksArray } from './task-management.slice';
interface GroupingState {
currentGrouping: TaskGrouping | null;
type GroupingType = 'status' | 'priority' | 'phase';
interface LocalGroupingState {
currentGrouping: GroupingType | null;
customPhases: string[];
groupOrder: {
status: string[];
@@ -12,10 +14,10 @@ interface GroupingState {
phase: string[];
};
groupStates: Record<string, { collapsed: boolean }>;
collapsedGroups: Set<string>;
collapsedGroups: string[];
}
const initialState: GroupingState = {
const initialState: LocalGroupingState = {
currentGrouping: null,
customPhases: ['Planning', 'Development', 'Testing', 'Deployment'],
groupOrder: {
@@ -24,14 +26,14 @@ const initialState: GroupingState = {
phase: ['Planning', 'Development', 'Testing', 'Deployment'],
},
groupStates: {},
collapsedGroups: new Set(),
collapsedGroups: [],
};
const groupingSlice = createSlice({
name: 'grouping',
initialState,
reducers: {
setCurrentGrouping: (state, action: PayloadAction<TaskGrouping | null>) => {
setCurrentGrouping: (state, action: PayloadAction<GroupingType | null>) => {
state.currentGrouping = action.payload;
},
@@ -54,20 +56,19 @@ const groupingSlice = createSlice({
state.groupOrder.phase = action.payload;
},
updateGroupOrder: (state, action: PayloadAction<{ groupType: string; order: string[] }>) => {
updateGroupOrder: (state, action: PayloadAction<{ groupType: keyof LocalGroupingState['groupOrder']; order: string[] }>) => {
const { groupType, order } = action.payload;
state.groupOrder[groupType] = order;
},
toggleGroupCollapsed: (state, action: PayloadAction<string>) => {
const groupId = action.payload;
const collapsedGroups = new Set(state.collapsedGroups);
if (collapsedGroups.has(groupId)) {
collapsedGroups.delete(groupId);
const isCollapsed = state.collapsedGroups.includes(groupId);
if (isCollapsed) {
state.collapsedGroups = state.collapsedGroups.filter(id => id !== groupId);
} else {
collapsedGroups.add(groupId);
state.collapsedGroups.push(groupId);
}
state.collapsedGroups = collapsedGroups;
},
setGroupCollapsed: (state, action: PayloadAction<{ groupId: string; collapsed: boolean }>) => {
@@ -79,11 +80,11 @@ const groupingSlice = createSlice({
},
collapseAllGroups: (state, action: PayloadAction<string[]>) => {
state.collapsedGroups = new Set(action.payload);
state.collapsedGroups = action.payload;
},
expandAllGroups: state => {
state.collapsedGroups = new Set();
state.collapsedGroups = [];
},
resetGrouping: () => initialState,
@@ -108,54 +109,59 @@ export const selectCurrentGrouping = (state: RootState) => state.grouping.curren
export const selectCustomPhases = (state: RootState) => state.grouping.customPhases;
export const selectGroupOrder = (state: RootState) => state.grouping.groupOrder;
export const selectGroupStates = (state: RootState) => state.grouping.groupStates;
export const selectCollapsedGroups = (state: RootState) => state.grouping.collapsedGroups;
export const selectCollapsedGroups = (state: RootState) => new Set(state.grouping.collapsedGroups);
export const selectIsGroupCollapsed = (state: RootState, groupId: string) =>
state.grouping.collapsedGroups.has(groupId);
state.grouping.collapsedGroups.includes(groupId);
// Complex selectors using createSelector for memoization
export const selectCurrentGroupOrder = createSelector(
[selectCurrentGrouping, selectGroupOrder],
(currentGrouping, groupOrder) => groupOrder[currentGrouping] || []
(currentGrouping, groupOrder) => {
if (!currentGrouping) return [];
return groupOrder[currentGrouping] || [];
}
);
export const selectTaskGroups = createSelector(
[selectAllTasks, selectCurrentGrouping, selectCurrentGroupOrder, selectGroupStates],
[selectAllTasksArray, selectCurrentGrouping, selectCurrentGroupOrder, selectGroupStates],
(tasks, currentGrouping, groupOrder, groupStates) => {
const groups: TaskGroup[] = [];
if (!currentGrouping) return groups;
// 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;
})
),
];
: Array.from(new Set(
tasks.map(task => {
if (currentGrouping === 'status') return task.status;
if (currentGrouping === 'priority') return task.priority;
return task.phase;
})
));
groupValues.forEach(value => {
if (!value) return; // Skip undefined values
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);
.sort((a, b) => (a.order || 0) - (b.order || 0));
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),
type: currentGrouping,
color: getGroupColor(currentGrouping, value),
collapsed: groupStates[groupId]?.collapsed || false,
groupValue: value,
});
});
@@ -164,15 +170,17 @@ export const selectTaskGroups = createSelector(
);
export const selectTasksByCurrentGrouping = createSelector(
[selectAllTasks, selectCurrentGrouping],
[selectAllTasksArray, selectCurrentGrouping],
(tasks, currentGrouping) => {
const grouped: Record<string, typeof tasks> = {};
if (!currentGrouping) return grouped;
tasks.forEach(task => {
let key: string;
if (currentGrouping === 'status') key = task.status;
else if (currentGrouping === 'priority') key = task.priority;
else key = task.phase;
else key = task.phase || 'Development';
if (!grouped[key]) grouped[key] = [];
grouped[key].push(task);
@@ -180,7 +188,7 @@ export const selectTasksByCurrentGrouping = createSelector(
// Sort tasks within each group by order
Object.keys(grouped).forEach(key => {
grouped[key].sort((a, b) => a.order - b.order);
grouped[key].sort((a, b) => (a.order || 0) - (b.order || 0));
});
return grouped;
@@ -188,7 +196,7 @@ export const selectTasksByCurrentGrouping = createSelector(
);
// Helper function to get group colors
const getGroupColor = (groupType: string, value: string): string => {
const getGroupColor = (groupType: GroupingType, value: string): string => {
const colorMaps = {
status: {
todo: '#f0f0f0',
@@ -209,7 +217,8 @@ const getGroupColor = (groupType: string, value: string): string => {
},
};
return colorMaps[groupType as keyof typeof colorMaps]?.[value as keyof any] || '#d9d9d9';
const colorMap = colorMaps[groupType];
return (colorMap as any)?.[value] || '#d9d9d9';
};
export default groupingSlice.reducer;

View File

@@ -14,6 +14,7 @@ import {
ITaskListV3Response,
} from '@/api/tasks/tasks.api.service';
import logger from '@/utils/errorLogger';
import { DEFAULT_TASK_NAME } from '@/shared/constants';
// Helper function to safely convert time values
const convertTimeValue = (value: any): number => {
@@ -137,7 +138,7 @@ export const fetchTasks = createAsyncThunk(
group.tasks.map((task: any) => ({
id: task.id,
task_key: task.task_key || '',
title: task.name || '',
title: (task.title && task.title.trim()) ? task.title.trim() : DEFAULT_TASK_NAME,
description: task.description || '',
status: statusIdToNameMap[task.status] || 'todo',
priority: priorityIdToNameMap[task.priority] || 'medium',
@@ -221,13 +222,17 @@ export const fetchTasksV3 = createAsyncThunk(
// Log raw response for debugging
console.log('Raw API response:', response.body);
console.log('Sample task from backend:', response.body.allTasks?.[0]);
// Ensure tasks are properly normalized
const tasks = response.body.allTasks.map((task: any) => {
const now = new Date().toISOString();
return {
id: task.id,
title: task.name || '',
title: (task.title && task.title.trim()) ? task.title.trim() : DEFAULT_TASK_NAME,
description: task.description || '',
status: task.status || 'todo',
priority: task.priority || 'medium',