feat(enhanced-kanban): implement group reordering and improve drag-and-drop functionality
- Added support for reordering kanban groups via drag-and-drop, enhancing user experience. - Updated EnhancedKanbanBoard and EnhancedKanbanGroup components to handle group dragging and state management. - Introduced visual feedback for dragging groups and tasks, improving usability. - Refined CSS styles for better layout and responsiveness during drag operations.
This commit is contained in:
@@ -26,6 +26,7 @@ import { RootState } from '@/app/store';
|
|||||||
import {
|
import {
|
||||||
fetchEnhancedKanbanGroups,
|
fetchEnhancedKanbanGroups,
|
||||||
reorderEnhancedKanbanTasks,
|
reorderEnhancedKanbanTasks,
|
||||||
|
reorderEnhancedKanbanGroups,
|
||||||
setDragState
|
setDragState
|
||||||
} from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
} from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
import EnhancedKanbanGroup from './EnhancedKanbanGroup';
|
import EnhancedKanbanGroup from './EnhancedKanbanGroup';
|
||||||
@@ -119,29 +120,43 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
|
|||||||
const handleDragStart = (event: DragStartEvent) => {
|
const handleDragStart = (event: DragStartEvent) => {
|
||||||
const { active } = event;
|
const { active } = event;
|
||||||
const activeId = active.id as string;
|
const activeId = active.id as string;
|
||||||
|
const activeData = active.data.current;
|
||||||
|
|
||||||
// Find the active task and group
|
// Check if dragging a group or a task
|
||||||
let foundTask = null;
|
if (activeData?.type === 'group') {
|
||||||
let foundGroup = null;
|
// Dragging a group
|
||||||
|
const foundGroup = taskGroups.find(g => g.id === activeId);
|
||||||
|
setActiveGroup(foundGroup);
|
||||||
|
setActiveTask(null);
|
||||||
|
|
||||||
for (const group of taskGroups) {
|
dispatch(setDragState({
|
||||||
const task = group.tasks.find(t => t.id === activeId);
|
activeTaskId: null,
|
||||||
if (task) {
|
activeGroupId: activeId,
|
||||||
foundTask = task;
|
isDragging: true,
|
||||||
foundGroup = group;
|
}));
|
||||||
break;
|
} else {
|
||||||
|
// Dragging a task
|
||||||
|
let foundTask = null;
|
||||||
|
let foundGroup = null;
|
||||||
|
|
||||||
|
for (const group of taskGroups) {
|
||||||
|
const task = group.tasks.find(t => t.id === activeId);
|
||||||
|
if (task) {
|
||||||
|
foundTask = task;
|
||||||
|
foundGroup = group;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setActiveTask(foundTask);
|
||||||
|
setActiveGroup(null);
|
||||||
|
|
||||||
|
dispatch(setDragState({
|
||||||
|
activeTaskId: activeId,
|
||||||
|
activeGroupId: foundGroup?.id || null,
|
||||||
|
isDragging: true,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
setActiveTask(foundTask);
|
|
||||||
setActiveGroup(foundGroup);
|
|
||||||
|
|
||||||
// Update Redux drag state
|
|
||||||
dispatch(setDragState({
|
|
||||||
activeTaskId: activeId,
|
|
||||||
activeGroupId: foundGroup?.id || null,
|
|
||||||
isDragging: true,
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragOver = (event: DragOverEvent) => {
|
const handleDragOver = (event: DragOverEvent) => {
|
||||||
@@ -164,6 +179,7 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
|
|||||||
|
|
||||||
const handleDragEnd = (event: DragEndEvent) => {
|
const handleDragEnd = (event: DragEndEvent) => {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
|
const activeData = active.data.current;
|
||||||
|
|
||||||
// Reset local state
|
// Reset local state
|
||||||
setActiveTask(null);
|
setActiveTask(null);
|
||||||
@@ -183,7 +199,28 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
|
|||||||
const activeId = active.id as string;
|
const activeId = active.id as string;
|
||||||
const overId = over.id as string;
|
const overId = over.id as string;
|
||||||
|
|
||||||
// Find source and target groups
|
// Handle group reordering
|
||||||
|
if (activeData?.type === 'group') {
|
||||||
|
const fromIndex = taskGroups.findIndex(g => g.id === activeId);
|
||||||
|
const toIndex = taskGroups.findIndex(g => g.id === overId);
|
||||||
|
|
||||||
|
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
|
||||||
|
// Create new array with reordered groups
|
||||||
|
const reorderedGroups = [...taskGroups];
|
||||||
|
const [movedGroup] = reorderedGroups.splice(fromIndex, 1);
|
||||||
|
reorderedGroups.splice(toIndex, 0, movedGroup);
|
||||||
|
|
||||||
|
// Dispatch group reorder action
|
||||||
|
dispatch(reorderEnhancedKanbanGroups({
|
||||||
|
fromIndex,
|
||||||
|
toIndex,
|
||||||
|
reorderedGroups,
|
||||||
|
}) as any);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle task reordering (existing logic)
|
||||||
let sourceGroup = null;
|
let sourceGroup = null;
|
||||||
let targetGroup = null;
|
let targetGroup = null;
|
||||||
let sourceIndex = -1;
|
let sourceIndex = -1;
|
||||||
@@ -309,6 +346,14 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
|
|||||||
isDragOverlay={true}
|
isDragOverlay={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{activeGroup && (
|
||||||
|
<div className="group-drag-overlay">
|
||||||
|
<div className="group-header-content">
|
||||||
|
<h3>{activeGroup.name}</h3>
|
||||||
|
<span className="task-count">({activeGroup.tasks.length})</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
.enhanced-kanban-group {
|
.enhanced-kanban-group {
|
||||||
min-width: 280px;
|
width: 300px;
|
||||||
max-width: 320px;
|
min-width: 300px;
|
||||||
|
max-width: 300px;
|
||||||
background: var(--ant-color-bg-elevated);
|
background: var(--ant-color-bg-elevated);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border: 1px solid var(--ant-color-border);
|
border: 1px solid var(--ant-color-border);
|
||||||
box-shadow: 0 1px 2px var(--ant-color-shadow);
|
box-shadow: 0 1px 2px var(--ant-color-shadow);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.enhanced-kanban-group.drag-over {
|
.enhanced-kanban-group.drag-over {
|
||||||
@@ -15,6 +18,16 @@
|
|||||||
box-shadow: 0 0 0 2px var(--ant-color-primary-border);
|
box-shadow: 0 0 0 2px var(--ant-color-primary-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.enhanced-kanban-group.group-dragging {
|
||||||
|
opacity: 0.5;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 8px 24px var(--ant-color-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.enhanced-kanban-group.group-dragging .enhanced-kanban-group-tasks {
|
||||||
|
background: var(--ant-color-bg-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
.enhanced-kanban-group-header {
|
.enhanced-kanban-group-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -22,21 +35,44 @@
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
border-bottom: 1px solid var(--ant-color-border);
|
border-bottom: 1px solid var(--ant-color-border);
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: -8px -8px 4px -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enhanced-kanban-group-header:active {
|
||||||
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.enhanced-kanban-group-header h3 {
|
.enhanced-kanban-group-header h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--ant-color-text);
|
color: inherit;
|
||||||
|
text-shadow: 0 1px 2px var(--ant-color-shadow);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 180px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-count {
|
.task-count {
|
||||||
background: var(--ant-color-fill-secondary);
|
background: var(--ant-color-bg-container);
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--ant-color-text-secondary);
|
color: var(--ant-color-text);
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid var(--ant-color-border);
|
||||||
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.virtualization-indicator {
|
.virtualization-indicator {
|
||||||
@@ -62,7 +98,9 @@
|
|||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
max-height: 600px;
|
max-height: 600px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
overflow: hidden;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Performance optimizations for large lists */
|
/* Performance optimizations for large lists */
|
||||||
@@ -127,6 +165,42 @@
|
|||||||
color: var(--ant-color-primary);
|
color: var(--ant-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Group drag overlay */
|
||||||
|
.group-drag-overlay {
|
||||||
|
background: var(--ant-color-bg-elevated);
|
||||||
|
border: 1px solid var(--ant-color-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
box-shadow: 0 8px 24px var(--ant-color-shadow);
|
||||||
|
min-width: 280px;
|
||||||
|
max-width: 320px;
|
||||||
|
opacity: 0.9;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-drag-overlay .group-header-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-drag-overlay h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-drag-overlay .task-count {
|
||||||
|
background: var(--ant-color-bg-container);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
border: 1px solid var(--ant-color-border);
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive design for different screen sizes */
|
/* Responsive design for different screen sizes */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.enhanced-kanban-group {
|
.enhanced-kanban-group {
|
||||||
@@ -148,4 +222,23 @@
|
|||||||
.enhanced-kanban-group-tasks {
|
.enhanced-kanban-group-tasks {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.enhanced-kanban-task-card {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-title {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--ant-color-text);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-word;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 220px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import React, { useMemo, useRef, useEffect, useState } from 'react';
|
import React, { useMemo, useRef, useEffect, useState } from 'react';
|
||||||
import { useDroppable } from '@dnd-kit/core';
|
import { useDroppable } from '@dnd-kit/core';
|
||||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
|
||||||
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||||
import EnhancedKanbanTaskCard from './EnhancedKanbanTaskCard';
|
import EnhancedKanbanTaskCard from './EnhancedKanbanTaskCard';
|
||||||
import VirtualizedTaskList from './VirtualizedTaskList';
|
import VirtualizedTaskList from './VirtualizedTaskList';
|
||||||
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
import './EnhancedKanbanGroup.css';
|
import './EnhancedKanbanGroup.css';
|
||||||
|
|
||||||
interface EnhancedKanbanGroupProps {
|
interface EnhancedKanbanGroupProps {
|
||||||
@@ -20,7 +22,25 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
activeTaskId,
|
activeTaskId,
|
||||||
overId
|
overId
|
||||||
}) => {
|
}) => {
|
||||||
const { setNodeRef, isOver } = useDroppable({
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
|
|
||||||
|
const { setNodeRef: setDroppableRef, isOver } = useDroppable({
|
||||||
|
id: group.id,
|
||||||
|
data: {
|
||||||
|
type: 'group',
|
||||||
|
group,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add sortable functionality for group header
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef: setSortableRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging: isGroupDragging,
|
||||||
|
} = useSortable({
|
||||||
id: group.id,
|
id: group.id,
|
||||||
data: {
|
data: {
|
||||||
type: 'group',
|
type: 'group',
|
||||||
@@ -67,13 +87,41 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
|
|||||||
// Performance optimization: Only render drop indicators when needed
|
// Performance optimization: Only render drop indicators when needed
|
||||||
const shouldShowDropIndicators = isDraggingOver && !shouldVirtualize;
|
const shouldShowDropIndicators = isDraggingOver && !shouldVirtualize;
|
||||||
|
|
||||||
|
// Combine refs for the main container
|
||||||
|
const setRefs = (el: HTMLElement | null) => {
|
||||||
|
setDroppableRef(el);
|
||||||
|
setSortableRef(el);
|
||||||
|
};
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isGroupDragging ? 0.5 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the appropriate background color based on theme
|
||||||
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setRefs}
|
||||||
className={`enhanced-kanban-group ${isDraggingOver ? 'drag-over' : ''}`}
|
style={style}
|
||||||
|
className={`enhanced-kanban-group ${isDraggingOver ? 'drag-over' : ''} ${isGroupDragging ? 'group-dragging' : ''}`}
|
||||||
>
|
>
|
||||||
<div className="enhanced-kanban-group-header">
|
<div
|
||||||
<h3>{group.name}</h3>
|
className="enhanced-kanban-group-header"
|
||||||
|
style={{
|
||||||
|
backgroundColor: headerBackgroundColor,
|
||||||
|
}}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
>
|
||||||
|
<h3 title={group.name}>{group.name}</h3>
|
||||||
<span className="task-count">({group.tasks.length})</span>
|
<span className="task-count">({group.tasks.length})</span>
|
||||||
{shouldVirtualize && (
|
{shouldVirtualize && (
|
||||||
<span className="virtualization-indicator" title="Virtualized for performance">
|
<span className="virtualization-indicator" title="Virtualized for performance">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { useSortable } from '@dnd-kit/sortable';
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||||
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
import './EnhancedKanbanTaskCard.css';
|
import './EnhancedKanbanTaskCard.css';
|
||||||
|
|
||||||
interface EnhancedKanbanTaskCardProps {
|
interface EnhancedKanbanTaskCardProps {
|
||||||
@@ -11,12 +12,14 @@ interface EnhancedKanbanTaskCardProps {
|
|||||||
isDropTarget?: boolean;
|
isDropTarget?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo(({
|
const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo(({
|
||||||
task,
|
task,
|
||||||
isActive = false,
|
isActive = false,
|
||||||
isDragOverlay = false,
|
isDragOverlay = false,
|
||||||
isDropTarget = false
|
isDropTarget = false
|
||||||
}) => {
|
}) => {
|
||||||
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
listeners,
|
listeners,
|
||||||
@@ -37,6 +40,7 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
|
|||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
opacity: isDragging ? 0.5 : 1,
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
backgroundColor: themeMode === 'dark' ? '#292929' : '#fafafa',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -48,7 +52,7 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
|
|||||||
{...listeners}
|
{...listeners}
|
||||||
>
|
>
|
||||||
<div className="task-content">
|
<div className="task-content">
|
||||||
<div className="task-title">{task.name}</div>
|
<div className="task-title" title={task.name}>{task.name}</div>
|
||||||
{/* {task.task_key && <div className="task-key">{task.task_key}</div>} */}
|
{/* {task.task_key && <div className="task-key">{task.task_key}</div>} */}
|
||||||
{task.assignees && task.assignees.length > 0 && (
|
{task.assignees && task.assignees.length > 0 && (
|
||||||
<div className="task-assignees">
|
<div className="task-assignees">
|
||||||
|
|||||||
@@ -49,12 +49,12 @@ interface EnhancedKanbanState {
|
|||||||
groupBy: IGroupBy;
|
groupBy: IGroupBy;
|
||||||
isSubtasksInclude: boolean;
|
isSubtasksInclude: boolean;
|
||||||
fields: ITaskListSortableColumn[];
|
fields: ITaskListSortableColumn[];
|
||||||
|
|
||||||
// Task data
|
// Task data
|
||||||
taskGroups: ITaskListGroup[];
|
taskGroups: ITaskListGroup[];
|
||||||
loadingGroups: boolean;
|
loadingGroups: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
taskAssignees: ITaskListMemberFilter[];
|
taskAssignees: ITaskListMemberFilter[];
|
||||||
loadingAssignees: boolean;
|
loadingAssignees: boolean;
|
||||||
@@ -63,12 +63,12 @@ interface EnhancedKanbanState {
|
|||||||
loadingLabels: boolean;
|
loadingLabels: boolean;
|
||||||
priorities: string[];
|
priorities: string[];
|
||||||
members: string[];
|
members: string[];
|
||||||
|
|
||||||
// Performance optimizations
|
// Performance optimizations
|
||||||
virtualizedRendering: boolean;
|
virtualizedRendering: boolean;
|
||||||
taskCache: Record<string, IProjectTask>;
|
taskCache: Record<string, IProjectTask>;
|
||||||
groupCache: Record<string, ITaskListGroup>;
|
groupCache: Record<string, ITaskListGroup>;
|
||||||
|
|
||||||
// Performance monitoring
|
// Performance monitoring
|
||||||
performanceMetrics: {
|
performanceMetrics: {
|
||||||
totalTasks: number;
|
totalTasks: number;
|
||||||
@@ -78,7 +78,7 @@ interface EnhancedKanbanState {
|
|||||||
lastUpdateTime: number;
|
lastUpdateTime: number;
|
||||||
virtualizationEnabled: boolean;
|
virtualizationEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Drag and drop state
|
// Drag and drop state
|
||||||
dragState: {
|
dragState: {
|
||||||
activeTaskId: string | null;
|
activeTaskId: string | null;
|
||||||
@@ -86,7 +86,7 @@ interface EnhancedKanbanState {
|
|||||||
overId: string | null;
|
overId: string | null;
|
||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
selectedTaskIds: string[];
|
selectedTaskIds: string[];
|
||||||
expandedSubtasks: Record<string, boolean>;
|
expandedSubtasks: Record<string, boolean>;
|
||||||
@@ -137,7 +137,7 @@ const calculatePerformanceMetrics = (taskGroups: ITaskListGroup[]) => {
|
|||||||
const groupSizes = taskGroups.map(group => group.tasks.length);
|
const groupSizes = taskGroups.map(group => group.tasks.length);
|
||||||
const largestGroupSize = Math.max(...groupSizes, 0);
|
const largestGroupSize = Math.max(...groupSizes, 0);
|
||||||
const averageGroupSize = groupSizes.length > 0 ? totalTasks / groupSizes.length : 0;
|
const averageGroupSize = groupSizes.length > 0 ? totalTasks / groupSizes.length : 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalTasks,
|
totalTasks,
|
||||||
largestGroupSize,
|
largestGroupSize,
|
||||||
@@ -234,6 +234,35 @@ export const reorderEnhancedKanbanTasks = createAsyncThunk(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Group reordering
|
||||||
|
export const reorderEnhancedKanbanGroups = createAsyncThunk(
|
||||||
|
'enhancedKanban/reorderGroups',
|
||||||
|
async (
|
||||||
|
{
|
||||||
|
fromIndex,
|
||||||
|
toIndex,
|
||||||
|
reorderedGroups,
|
||||||
|
}: {
|
||||||
|
fromIndex: number;
|
||||||
|
toIndex: number;
|
||||||
|
reorderedGroups: ITaskListGroup[];
|
||||||
|
},
|
||||||
|
{ rejectWithValue }
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
// Optimistic update - return immediately for UI responsiveness
|
||||||
|
return {
|
||||||
|
fromIndex,
|
||||||
|
toIndex,
|
||||||
|
reorderedGroups,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Reorder Enhanced Kanban Groups', error);
|
||||||
|
return rejectWithValue('Failed to reorder groups');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const enhancedKanbanSlice = createSlice({
|
const enhancedKanbanSlice = createSlice({
|
||||||
name: 'enhancedKanbanReducer',
|
name: 'enhancedKanbanReducer',
|
||||||
initialState,
|
initialState,
|
||||||
@@ -327,7 +356,7 @@ const enhancedKanbanSlice = createSlice({
|
|||||||
// Status updates
|
// Status updates
|
||||||
updateTaskStatus: (state, action: PayloadAction<ITaskListStatusChangeResponse>) => {
|
updateTaskStatus: (state, action: PayloadAction<ITaskListStatusChangeResponse>) => {
|
||||||
const { id: task_id, status_id } = action.payload;
|
const { id: task_id, status_id } = action.payload;
|
||||||
|
|
||||||
// Update in all groups
|
// Update in all groups
|
||||||
state.taskGroups.forEach(group => {
|
state.taskGroups.forEach(group => {
|
||||||
group.tasks.forEach(task => {
|
group.tasks.forEach(task => {
|
||||||
@@ -342,7 +371,7 @@ const enhancedKanbanSlice = createSlice({
|
|||||||
|
|
||||||
updateTaskPriority: (state, action: PayloadAction<ITaskListPriorityChangeResponse>) => {
|
updateTaskPriority: (state, action: PayloadAction<ITaskListPriorityChangeResponse>) => {
|
||||||
const { id: task_id, priority_id } = action.payload;
|
const { id: task_id, priority_id } = action.payload;
|
||||||
|
|
||||||
// Update in all groups
|
// Update in all groups
|
||||||
state.taskGroups.forEach(group => {
|
state.taskGroups.forEach(group => {
|
||||||
group.tasks.forEach(task => {
|
group.tasks.forEach(task => {
|
||||||
@@ -358,12 +387,12 @@ const enhancedKanbanSlice = createSlice({
|
|||||||
// Task deletion
|
// Task deletion
|
||||||
deleteTask: (state, action: PayloadAction<string>) => {
|
deleteTask: (state, action: PayloadAction<string>) => {
|
||||||
const taskId = action.payload;
|
const taskId = action.payload;
|
||||||
|
|
||||||
// Remove from all groups
|
// Remove from all groups
|
||||||
state.taskGroups.forEach(group => {
|
state.taskGroups.forEach(group => {
|
||||||
group.tasks = group.tasks.filter(task => task.id !== taskId);
|
group.tasks = group.tasks.filter(task => task.id !== taskId);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove from caches
|
// Remove from caches
|
||||||
delete state.taskCache[taskId];
|
delete state.taskCache[taskId];
|
||||||
state.selectedTaskIds = state.selectedTaskIds.filter(id => id !== taskId);
|
state.selectedTaskIds = state.selectedTaskIds.filter(id => id !== taskId);
|
||||||
@@ -383,10 +412,10 @@ const enhancedKanbanSlice = createSlice({
|
|||||||
.addCase(fetchEnhancedKanbanGroups.fulfilled, (state, action) => {
|
.addCase(fetchEnhancedKanbanGroups.fulfilled, (state, action) => {
|
||||||
state.loadingGroups = false;
|
state.loadingGroups = false;
|
||||||
state.taskGroups = action.payload;
|
state.taskGroups = action.payload;
|
||||||
|
|
||||||
// Update performance metrics
|
// Update performance metrics
|
||||||
state.performanceMetrics = calculatePerformanceMetrics(action.payload);
|
state.performanceMetrics = calculatePerformanceMetrics(action.payload);
|
||||||
|
|
||||||
// Update caches
|
// Update caches
|
||||||
action.payload.forEach(group => {
|
action.payload.forEach(group => {
|
||||||
state.groupCache[group.id] = group;
|
state.groupCache[group.id] = group;
|
||||||
@@ -394,7 +423,7 @@ const enhancedKanbanSlice = createSlice({
|
|||||||
state.taskCache[task.id!] = task;
|
state.taskCache[task.id!] = task;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize column order if not set
|
// Initialize column order if not set
|
||||||
if (state.columnOrder.length === 0) {
|
if (state.columnOrder.length === 0) {
|
||||||
state.columnOrder = action.payload.map(group => group.id);
|
state.columnOrder = action.payload.map(group => group.id);
|
||||||
@@ -406,20 +435,33 @@ const enhancedKanbanSlice = createSlice({
|
|||||||
})
|
})
|
||||||
.addCase(reorderEnhancedKanbanTasks.fulfilled, (state, action) => {
|
.addCase(reorderEnhancedKanbanTasks.fulfilled, (state, action) => {
|
||||||
const { activeGroupId, overGroupId, updatedSourceTasks, updatedTargetTasks } = action.payload;
|
const { activeGroupId, overGroupId, updatedSourceTasks, updatedTargetTasks } = action.payload;
|
||||||
|
|
||||||
// Update groups
|
// Update groups
|
||||||
const sourceGroupIndex = state.taskGroups.findIndex(group => group.id === activeGroupId);
|
const sourceGroupIndex = state.taskGroups.findIndex(group => group.id === activeGroupId);
|
||||||
const targetGroupIndex = state.taskGroups.findIndex(group => group.id === overGroupId);
|
const targetGroupIndex = state.taskGroups.findIndex(group => group.id === overGroupId);
|
||||||
|
|
||||||
if (sourceGroupIndex !== -1) {
|
if (sourceGroupIndex !== -1) {
|
||||||
state.taskGroups[sourceGroupIndex].tasks = updatedSourceTasks;
|
state.taskGroups[sourceGroupIndex].tasks = updatedSourceTasks;
|
||||||
state.groupCache[activeGroupId] = state.taskGroups[sourceGroupIndex];
|
state.groupCache[activeGroupId] = state.taskGroups[sourceGroupIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetGroupIndex !== -1 && activeGroupId !== overGroupId) {
|
if (targetGroupIndex !== -1 && activeGroupId !== overGroupId) {
|
||||||
state.taskGroups[targetGroupIndex].tasks = updatedTargetTasks;
|
state.taskGroups[targetGroupIndex].tasks = updatedTargetTasks;
|
||||||
state.groupCache[overGroupId] = state.taskGroups[targetGroupIndex];
|
state.groupCache[overGroupId] = state.taskGroups[targetGroupIndex];
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.addCase(reorderEnhancedKanbanGroups.fulfilled, (state, action) => {
|
||||||
|
const { fromIndex, toIndex, reorderedGroups } = action.payload;
|
||||||
|
|
||||||
|
// Update groups
|
||||||
|
state.taskGroups = reorderedGroups;
|
||||||
|
state.groupCache = reorderedGroups.reduce((cache, group) => {
|
||||||
|
cache[group.id] = group;
|
||||||
|
return cache;
|
||||||
|
}, {} as Record<string, ITaskListGroup>);
|
||||||
|
|
||||||
|
// Update column order
|
||||||
|
state.columnOrder = reorderedGroups.map(group => group.id);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user