feat(enhanced-kanban): enhance Kanban board with improved task filtering and loading states

- Integrated ImprovedTaskFilters component for better task management.
- Added loading and error handling states to the Kanban board for improved user experience.
- Updated drag-and-drop functionality to dispatch actions for reordering tasks and groups directly from Redux state.
This commit is contained in:
shancds
2025-07-02 15:42:53 +05:30
parent d70fb133b7
commit 11e5a6d379

View File

@@ -1,11 +1,18 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { useSelector } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '@/app/store'; import { RootState } from '@/app/store';
import { ITaskListGroup } from '@/types/tasks/taskList.types'; import { ITaskListGroup } from '@/types/tasks/taskList.types';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types'; import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import './EnhancedKanbanBoard.css'; import './EnhancedKanbanBoard.css';
import './EnhancedKanbanGroup.css'; import './EnhancedKanbanGroup.css';
import './EnhancedKanbanTaskCard.css'; import './EnhancedKanbanTaskCard.css';
import ImprovedTaskFilters from '../task-management/improved-task-filters';
import Card from 'antd/es/card';
import Spin from 'antd/es/spin';
import Empty from 'antd/es/empty';
import { reorderGroups, reorderEnhancedKanbanGroups, reorderTasks, reorderEnhancedKanbanTasks, fetchEnhancedKanbanLabels, fetchEnhancedKanbanGroups, fetchEnhancedKanbanTaskAssignees } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
import { useAppSelector } from '@/hooks/useAppSelector';
// Minimal task card for prototype (reuse your styles) // Minimal task card for prototype (reuse your styles)
const TaskCard: React.FC<{ const TaskCard: React.FC<{
@@ -55,13 +62,22 @@ const KanbanGroup: React.FC<{
onTaskDrop: (e: React.DragEvent, groupId: string, taskIdx: number) => void; onTaskDrop: (e: React.DragEvent, groupId: string, taskIdx: number) => void;
hoveredTaskIdx: number | null; hoveredTaskIdx: number | null;
hoveredGroupId: string | null; hoveredGroupId: string | null;
}> = ({ group, onGroupDragStart, onGroupDragOver, onGroupDrop, onTaskDragStart, onTaskDragOver, onTaskDrop, hoveredTaskIdx, hoveredGroupId }) => ( }> = ({ group, onGroupDragStart, onGroupDragOver, onGroupDrop, onTaskDragStart, onTaskDragOver, onTaskDrop, hoveredTaskIdx, hoveredGroupId }) => {
<div const themeMode = useAppSelector(state => state.themeReducer.mode);
className="enhanced-kanban-group"
// Only group header is draggable for group drag const headerBackgroundColor = useMemo(() => {
> if (themeMode === 'dark') {
return group.color_code_dark || group.color_code || '#1e1e1e';
}
return group.color_code || '#f5f5f5';
}, [themeMode, group.color_code, group.color_code_dark]);
return (
<div className="enhanced-kanban-group">
<div <div
className="enhanced-kanban-group-header" className="enhanced-kanban-group-header"
style={{
backgroundColor: headerBackgroundColor,
}}
draggable draggable
onDragStart={e => onGroupDragStart(e, group.id)} onDragStart={e => onGroupDragStart(e, group.id)}
onDragOver={onGroupDragOver} onDragOver={onGroupDragOver}
@@ -96,25 +112,42 @@ const KanbanGroup: React.FC<{
)} )}
</div> </div>
</div> </div>
); )};
const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ projectId }) => { const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ projectId }) => {
// Get initial groups from Redux const dispatch = useDispatch();
const reduxGroups = useSelector((state: RootState) => state.enhancedKanbanReducer.taskGroups); const {
// Local state for groups/tasks taskGroups,
const [groups, setGroups] = useState<ITaskListGroup[]>([]); loadingGroups,
// Drag state error,
} = useSelector((state: RootState) => state.enhancedKanbanReducer);
const [draggedGroupId, setDraggedGroupId] = useState<string | null>(null); const [draggedGroupId, setDraggedGroupId] = useState<string | null>(null);
const [draggedTaskId, setDraggedTaskId] = useState<string | null>(null); const [draggedTaskId, setDraggedTaskId] = useState<string | null>(null);
const [draggedTaskGroupId, setDraggedTaskGroupId] = useState<string | null>(null); const [draggedTaskGroupId, setDraggedTaskGroupId] = useState<string | null>(null);
const [hoveredGroupId, setHoveredGroupId] = useState<string | null>(null); const [hoveredGroupId, setHoveredGroupId] = useState<string | null>(null);
const [hoveredTaskIdx, setHoveredTaskIdx] = useState<number | null>(null); const [hoveredTaskIdx, setHoveredTaskIdx] = useState<number | null>(null);
const [dragType, setDragType] = useState<'group' | 'task' | null>(null); const [dragType, setDragType] = useState<'group' | 'task' | null>(null);
const { statusCategories, status: existingStatuses } = useAppSelector((state) => state.taskStatusReducer);
// Sync local state with Redux on mount or when reduxGroups or projectId change
useEffect(() => { useEffect(() => {
setGroups(reduxGroups.map(g => ({ ...g, tasks: [...g.tasks] }))); if (projectId) {
}, [reduxGroups, projectId]); dispatch(fetchEnhancedKanbanGroups(projectId) as any);
// Load filter data for enhanced kanban
dispatch(fetchEnhancedKanbanTaskAssignees(projectId) as any);
dispatch(fetchEnhancedKanbanLabels(projectId) as any);
}
if (!statusCategories.length) {
dispatch(fetchStatusesCategories() as any);
}
}, [dispatch, projectId]);
// Reset drag state if taskGroups changes (e.g., real-time update)
useEffect(() => {
setDraggedGroupId(null);
setDraggedTaskId(null);
setDraggedTaskGroupId(null);
setHoveredGroupId(null);
setHoveredTaskIdx(null);
setDragType(null);
}, [taskGroups]);
// Group drag handlers // Group drag handlers
const handleGroupDragStart = (e: React.DragEvent, groupId: string) => { const handleGroupDragStart = (e: React.DragEvent, groupId: string) => {
@@ -130,12 +163,15 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
if (dragType !== 'group') return; if (dragType !== 'group') return;
e.preventDefault(); e.preventDefault();
if (!draggedGroupId || draggedGroupId === targetGroupId) return; if (!draggedGroupId || draggedGroupId === targetGroupId) return;
const updated = [...groups]; // Calculate new order and dispatch
const fromIdx = updated.findIndex(g => g.id === draggedGroupId); const fromIdx = taskGroups.findIndex(g => g.id === draggedGroupId);
const [moved] = updated.splice(fromIdx, 1); const toIdx = taskGroups.findIndex(g => g.id === targetGroupId);
const toIdx = updated.findIndex(g => g.id === targetGroupId); if (fromIdx === -1 || toIdx === -1) return;
updated.splice(toIdx, 0, moved); const reorderedGroups = [...taskGroups];
setGroups(updated); const [moved] = reorderedGroups.splice(fromIdx, 1);
reorderedGroups.splice(toIdx, 0, moved);
dispatch(reorderGroups({ fromIndex: fromIdx, toIndex: toIdx, reorderedGroups }));
dispatch(reorderEnhancedKanbanGroups({ fromIndex: fromIdx, toIndex: toIdx, reorderedGroups }) as any);
setDraggedGroupId(null); setDraggedGroupId(null);
setDragType(null); setDragType(null);
}; };
@@ -159,23 +195,44 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
if (dragType !== 'task') return; if (dragType !== 'task') return;
e.preventDefault(); e.preventDefault();
if (!draggedTaskId || !draggedTaskGroupId || hoveredGroupId === null || hoveredTaskIdx === null) return; if (!draggedTaskId || !draggedTaskGroupId || hoveredGroupId === null || hoveredTaskIdx === null) return;
const updated = [...groups]; // Calculate new order and dispatch
const sourceGroup = updated.find(g => g.id === draggedTaskGroupId); const sourceGroup = taskGroups.find(g => g.id === draggedTaskGroupId);
const targetGroup = updated.find(g => g.id === targetGroupId); const targetGroup = taskGroups.find(g => g.id === targetGroupId);
if (!sourceGroup || !targetGroup) return; if (!sourceGroup || !targetGroup) return;
// Remove from source
const taskIdx = sourceGroup.tasks.findIndex(t => t.id === draggedTaskId); const taskIdx = sourceGroup.tasks.findIndex(t => t.id === draggedTaskId);
if (taskIdx === -1) return; if (taskIdx === -1) return;
const [movedTask] = sourceGroup.tasks.splice(taskIdx, 1); const movedTask = sourceGroup.tasks[taskIdx];
// Insert into target at the correct index // Prepare updated task arrays
const updatedSourceTasks = [...sourceGroup.tasks];
updatedSourceTasks.splice(taskIdx, 1);
let insertIdx = targetTaskIdx; let insertIdx = targetTaskIdx;
if (sourceGroup.id === targetGroup.id && taskIdx < insertIdx) { if (sourceGroup.id === targetGroup.id && taskIdx < insertIdx) {
insertIdx--; insertIdx--;
} }
if (insertIdx < 0) insertIdx = 0; if (insertIdx < 0) insertIdx = 0;
if (insertIdx > targetGroup.tasks.length) insertIdx = targetGroup.tasks.length; if (insertIdx > targetGroup.tasks.length) insertIdx = targetGroup.tasks.length;
targetGroup.tasks.splice(insertIdx, 0, movedTask); const updatedTargetTasks = sourceGroup.id === targetGroup.id
setGroups(updated); ? [...updatedSourceTasks]
: [...targetGroup.tasks];
updatedTargetTasks.splice(insertIdx, 0, movedTask);
dispatch(reorderTasks({
activeGroupId: sourceGroup.id,
overGroupId: targetGroup.id,
fromIndex: taskIdx,
toIndex: insertIdx,
task: movedTask,
updatedSourceTasks,
updatedTargetTasks,
}));
dispatch(reorderEnhancedKanbanTasks({
activeGroupId: sourceGroup.id,
overGroupId: targetGroup.id,
fromIndex: taskIdx,
toIndex: insertIdx,
task: movedTask,
updatedSourceTasks,
updatedTargetTasks,
}) as any);
setDraggedTaskId(null); setDraggedTaskId(null);
setDraggedTaskGroupId(null); setDraggedTaskGroupId(null);
setHoveredGroupId(null); setHoveredGroupId(null);
@@ -183,25 +240,52 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
setDragType(null); setDragType(null);
}; };
if (error) {
return (
<Card>
<Empty description={`Error loading tasks: ${error}`} image={Empty.PRESENTED_IMAGE_SIMPLE} />
</Card>
);
}
return ( return (
<div className="enhanced-kanban-board"> <>
<div className="kanban-groups-container"> <div className="mb-4">
{groups.map(group => ( <React.Suspense fallback={<div>Loading filters...</div>}>
<KanbanGroup <ImprovedTaskFilters position="board" />
key={group.id} </React.Suspense>
group={group}
onGroupDragStart={handleGroupDragStart}
onGroupDragOver={handleGroupDragOver}
onGroupDrop={handleGroupDrop}
onTaskDragStart={handleTaskDragStart}
onTaskDragOver={handleTaskDragOver}
onTaskDrop={handleTaskDrop}
hoveredTaskIdx={hoveredGroupId === group.id ? hoveredTaskIdx : null}
hoveredGroupId={hoveredGroupId}
/>
))}
</div> </div>
</div> <div className="enhanced-kanban-board">
{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>
) : (
<div className="kanban-groups-container">
{taskGroups.map(group => (
<KanbanGroup
key={group.id}
group={group}
onGroupDragStart={handleGroupDragStart}
onGroupDragOver={handleGroupDragOver}
onGroupDrop={handleGroupDrop}
onTaskDragStart={handleTaskDragStart}
onTaskDragOver={handleTaskDragOver}
onTaskDrop={handleTaskDrop}
hoveredTaskIdx={hoveredGroupId === group.id ? hoveredTaskIdx : null}
hoveredGroupId={hoveredGroupId}
/>
))}
</div>
)}
</div>
</>
); );
}; };