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:
@@ -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,10 +240,35 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
|
|||||||
setDragType(null);
|
setDragType(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
return (
|
return (
|
||||||
|
<Card>
|
||||||
|
<Empty description={`Error loading tasks: ${error}`} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-4">
|
||||||
|
<React.Suspense fallback={<div>Loading filters...</div>}>
|
||||||
|
<ImprovedTaskFilters position="board" />
|
||||||
|
</React.Suspense>
|
||||||
|
</div>
|
||||||
<div className="enhanced-kanban-board">
|
<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">
|
<div className="kanban-groups-container">
|
||||||
{groups.map(group => (
|
{taskGroups.map(group => (
|
||||||
<KanbanGroup
|
<KanbanGroup
|
||||||
key={group.id}
|
key={group.id}
|
||||||
group={group}
|
group={group}
|
||||||
@@ -201,7 +283,9 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user