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:
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user