Merge pull request #269 from shancds/test/row-kanban-board-v1.2.0
Test/row kanban board v1.2.0
This commit is contained in:
@@ -19,7 +19,6 @@ import { useAuthService } from '@/hooks/useAuth';
|
|||||||
import { statusApiService } from '@/api/taskAttributes/status/status.api.service';
|
import { statusApiService } from '@/api/taskAttributes/status/status.api.service';
|
||||||
import alertService from '@/services/alerts/alertService';
|
import alertService from '@/services/alerts/alertService';
|
||||||
import logger from '@/utils/errorLogger';
|
import logger from '@/utils/errorLogger';
|
||||||
import Skeleton from 'antd/es/skeleton/Skeleton';
|
|
||||||
import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status';
|
import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status';
|
||||||
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
|
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
|
||||||
|
|
||||||
@@ -148,11 +147,11 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
|
|||||||
const targetGroup = taskGroups.find(g => g.id === targetGroupId);
|
const targetGroup = taskGroups.find(g => g.id === targetGroupId);
|
||||||
if (!sourceGroup || !targetGroup) return;
|
if (!sourceGroup || !targetGroup) return;
|
||||||
|
|
||||||
|
|
||||||
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[taskIdx];
|
const movedTask = sourceGroup.tasks[taskIdx];
|
||||||
|
let didStatusChange = false;
|
||||||
if (groupBy === 'status' && movedTask.id) {
|
if (groupBy === 'status' && movedTask.id) {
|
||||||
if (sourceGroup.id !== targetGroup.id) {
|
if (sourceGroup.id !== targetGroup.id) {
|
||||||
const canContinue = await checkTaskDependencyStatus(movedTask.id, targetGroupId);
|
const canContinue = await checkTaskDependencyStatus(movedTask.id, targetGroupId);
|
||||||
@@ -163,6 +162,7 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
didStatusChange = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let insertIdx = hoveredTaskIdx;
|
let insertIdx = hoveredTaskIdx;
|
||||||
@@ -259,6 +259,18 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
|
|||||||
team_id: teamId,
|
team_id: teamId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Emit progress update if status changed
|
||||||
|
if (didStatusChange) {
|
||||||
|
socket.emit(
|
||||||
|
SocketEvents.TASK_STATUS_CHANGE.toString(),
|
||||||
|
JSON.stringify({
|
||||||
|
task_id: movedTask.id,
|
||||||
|
status_id: targetGroupId,
|
||||||
|
parent_task: movedTask.parent_task_id || null,
|
||||||
|
team_id: teamId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setDraggedTaskId(null);
|
setDraggedTaskId(null);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { SocketEvents } from '@/shared/socket-events';
|
|||||||
import { getUserSession } from '@/utils/session-helper';
|
import { getUserSession } from '@/utils/session-helper';
|
||||||
import { themeWiseColor } from '@/utils/themeWiseColor';
|
import { themeWiseColor } from '@/utils/themeWiseColor';
|
||||||
import { toggleTaskExpansion, fetchBoardSubTasks } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
import { toggleTaskExpansion, fetchBoardSubTasks } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
|
import TaskProgressCircle from './TaskProgressCircle';
|
||||||
|
|
||||||
// Simple Portal component
|
// Simple Portal component
|
||||||
const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
@@ -69,7 +70,6 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
|
|||||||
const d = selectedDate || new Date();
|
const d = selectedDate || new Date();
|
||||||
return new Date(d.getFullYear(), d.getMonth(), 1);
|
return new Date(d.getFullYear(), d.getMonth(), 1);
|
||||||
});
|
});
|
||||||
const [showSubtasks, setShowSubtasks] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedDate(task.end_date ? new Date(task.end_date) : null);
|
setSelectedDate(task.end_date ? new Date(task.end_date) : null);
|
||||||
@@ -202,7 +202,11 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="enhanced-kanban-task-card" style={{ background, color, display: 'block' }} >
|
<div className="enhanced-kanban-task-card" style={{ background, color, display: 'block', position: 'relative' }} >
|
||||||
|
{/* Progress circle at top right */}
|
||||||
|
<div style={{ position: 'absolute', top: 6, right: 6, zIndex: 2 }}>
|
||||||
|
<TaskProgressCircle task={task} size={20} />
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
draggable
|
draggable
|
||||||
onDragStart={e => onTaskDragStart(e, task.id!, groupId)}
|
onDragStart={e => onTaskDragStart(e, task.id!, groupId)}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { IProjectTask } from "@/types/project/projectTasksViewModel.types";
|
||||||
|
|
||||||
|
// Add a simple circular progress component
|
||||||
|
const TaskProgressCircle: React.FC<{ task: IProjectTask; size?: number }> = ({ task, size = 28 }) => {
|
||||||
|
const progress = typeof task.complete_ratio === 'number'
|
||||||
|
? task.complete_ratio
|
||||||
|
: (typeof task.progress === 'number' ? task.progress : 0);
|
||||||
|
const strokeWidth = 1.5;
|
||||||
|
const radius = (size - strokeWidth) / 2;
|
||||||
|
const circumference = 2 * Math.PI * radius;
|
||||||
|
const offset = circumference - (progress / 100) * circumference;
|
||||||
|
return (
|
||||||
|
<svg width={size} height={size} style={{ display: 'block' }}>
|
||||||
|
|
||||||
|
<circle
|
||||||
|
cx={size / 2}
|
||||||
|
cy={size / 2}
|
||||||
|
r={radius}
|
||||||
|
stroke="#3b82f6"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fill="none"
|
||||||
|
strokeDasharray={circumference}
|
||||||
|
strokeDashoffset={offset}
|
||||||
|
strokeLinecap="round"
|
||||||
|
style={{ transition: 'stroke-dashoffset 0.3s' }}
|
||||||
|
/>
|
||||||
|
{task.complete_ratio && <text
|
||||||
|
x="50%"
|
||||||
|
y="50%"
|
||||||
|
textAnchor="middle"
|
||||||
|
dominantBaseline="central"
|
||||||
|
fontSize={size * 0.38}
|
||||||
|
fill="#3b82f6"
|
||||||
|
fontWeight="bold"
|
||||||
|
>
|
||||||
|
{Math.round(progress)}
|
||||||
|
</text>}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskProgressCircle;
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
.performance-monitor {
|
|
||||||
position: fixed;
|
|
||||||
top: 80px;
|
|
||||||
right: 16px;
|
|
||||||
width: 280px;
|
|
||||||
z-index: 1000;
|
|
||||||
background: var(--ant-color-bg-elevated);
|
|
||||||
border: 1px solid var(--ant-color-border);
|
|
||||||
box-shadow: 0 4px 12px var(--ant-color-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-monitor-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--ant-color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-status {
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-metrics {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-metrics .ant-statistic {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-metrics .ant-statistic-title {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--ant-color-text-secondary);
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-metrics .ant-statistic-content {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--ant-color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.virtualization-status {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 0;
|
|
||||||
border-top: 1px solid var(--ant-color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-label {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--ant-color-text-secondary);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-tips {
|
|
||||||
margin-top: 12px;
|
|
||||||
padding-top: 12px;
|
|
||||||
border-top: 1px solid var(--ant-color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-tips h4 {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--ant-color-text);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-tips ul {
|
|
||||||
margin: 0;
|
|
||||||
padding-left: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-tips li {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--ant-color-text-secondary);
|
|
||||||
margin-bottom: 4px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive design */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.performance-monitor {
|
|
||||||
position: static;
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performance-metrics {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Card, Statistic, Tooltip, Badge } from 'antd';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { RootState } from '@/app/store';
|
|
||||||
import './PerformanceMonitor.css';
|
|
||||||
|
|
||||||
const PerformanceMonitor: React.FC = () => {
|
|
||||||
const { performanceMetrics } = useSelector((state: RootState) => state.enhancedKanbanReducer);
|
|
||||||
|
|
||||||
// Only show if there are tasks loaded
|
|
||||||
if (performanceMetrics.totalTasks === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPerformanceStatus = () => {
|
|
||||||
if (performanceMetrics.totalTasks > 1000) return 'critical';
|
|
||||||
if (performanceMetrics.totalTasks > 500) return 'warning';
|
|
||||||
if (performanceMetrics.totalTasks > 100) return 'good';
|
|
||||||
return 'excellent';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'critical':
|
|
||||||
return 'red';
|
|
||||||
case 'warning':
|
|
||||||
return 'orange';
|
|
||||||
case 'good':
|
|
||||||
return 'blue';
|
|
||||||
case 'excellent':
|
|
||||||
return 'green';
|
|
||||||
default:
|
|
||||||
return 'default';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const status = getPerformanceStatus();
|
|
||||||
const statusColor = getStatusColor(status);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
size="small"
|
|
||||||
className="performance-monitor"
|
|
||||||
title={
|
|
||||||
<div className="performance-monitor-header">
|
|
||||||
<span>Performance Monitor</span>
|
|
||||||
<Badge
|
|
||||||
status={statusColor as any}
|
|
||||||
text={status.toUpperCase()}
|
|
||||||
className="performance-status"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="performance-metrics">
|
|
||||||
<Tooltip title="Total number of tasks across all groups">
|
|
||||||
<Statistic
|
|
||||||
title="Total Tasks"
|
|
||||||
value={performanceMetrics.totalTasks}
|
|
||||||
suffix="tasks"
|
|
||||||
valueStyle={{ fontSize: '16px' }}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip title="Largest group by number of tasks">
|
|
||||||
<Statistic
|
|
||||||
title="Largest Group"
|
|
||||||
value={performanceMetrics.largestGroupSize}
|
|
||||||
suffix="tasks"
|
|
||||||
valueStyle={{ fontSize: '16px' }}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip title="Average tasks per group">
|
|
||||||
<Statistic
|
|
||||||
title="Average Group"
|
|
||||||
value={Math.round(performanceMetrics.averageGroupSize)}
|
|
||||||
suffix="tasks"
|
|
||||||
valueStyle={{ fontSize: '16px' }}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip title="Virtualization is enabled for groups with more than 50 tasks">
|
|
||||||
<div className="virtualization-status">
|
|
||||||
<span className="status-label">Virtualization:</span>
|
|
||||||
<Badge
|
|
||||||
status={performanceMetrics.virtualizationEnabled ? 'success' : 'default'}
|
|
||||||
text={performanceMetrics.virtualizationEnabled ? 'Enabled' : 'Disabled'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{performanceMetrics.totalTasks > 500 && (
|
|
||||||
<div className="performance-tips">
|
|
||||||
<h4>Performance Tips:</h4>
|
|
||||||
<ul>
|
|
||||||
<li>Use filters to reduce the number of visible tasks</li>
|
|
||||||
<li>Consider grouping by different criteria</li>
|
|
||||||
<li>Virtualization is automatically enabled for large groups</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default React.memo(PerformanceMonitor);
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
.virtualized-task-list {
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.virtualized-task-row {
|
|
||||||
padding: 4px 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.virtualized-empty-state {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: var(--ant-color-bg-container);
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 2px dashed var(--ant-color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-message {
|
|
||||||
color: var(--ant-color-text-secondary);
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure virtualized list works well with drag and drop */
|
|
||||||
.virtualized-task-list .react-window__inner {
|
|
||||||
overflow: visible !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Performance optimizations */
|
|
||||||
.virtualized-task-list * {
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Smooth scrolling */
|
|
||||||
.virtualized-task-list {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom scrollbar for better UX */
|
|
||||||
.virtualized-task-list::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.virtualized-task-list::-webkit-scrollbar-track {
|
|
||||||
background: var(--ant-color-bg-container);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.virtualized-task-list::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--ant-color-border);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.virtualized-task-list::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--ant-color-text-tertiary);
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import React, { useMemo, useCallback } from 'react';
|
|
||||||
import { FixedSizeList as List } from 'react-window';
|
|
||||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
|
||||||
import EnhancedKanbanTaskCard from './EnhancedKanbanTaskCard';
|
|
||||||
import './VirtualizedTaskList.css';
|
|
||||||
|
|
||||||
interface VirtualizedTaskListProps {
|
|
||||||
tasks: IProjectTask[];
|
|
||||||
height: number;
|
|
||||||
itemHeight?: number;
|
|
||||||
activeTaskId?: string | null;
|
|
||||||
overId?: string | null;
|
|
||||||
onTaskRender?: (task: IProjectTask, index: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = ({
|
|
||||||
tasks,
|
|
||||||
height,
|
|
||||||
itemHeight = 80,
|
|
||||||
activeTaskId,
|
|
||||||
overId,
|
|
||||||
onTaskRender,
|
|
||||||
}) => {
|
|
||||||
// Memoize task data to prevent unnecessary re-renders
|
|
||||||
const taskData = useMemo(
|
|
||||||
() => ({
|
|
||||||
tasks,
|
|
||||||
activeTaskId,
|
|
||||||
overId,
|
|
||||||
onTaskRender,
|
|
||||||
}),
|
|
||||||
[tasks, activeTaskId, overId, onTaskRender]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Row renderer for virtualized list
|
|
||||||
const Row = useCallback(
|
|
||||||
({ index, style }: { index: number; style: React.CSSProperties }) => {
|
|
||||||
const task = tasks[index];
|
|
||||||
if (!task) return null;
|
|
||||||
|
|
||||||
// Call onTaskRender callback if provided
|
|
||||||
onTaskRender?.(task, index);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EnhancedKanbanTaskCard
|
|
||||||
task={task}
|
|
||||||
isActive={task.id === activeTaskId}
|
|
||||||
isDropTarget={overId === task.id}
|
|
||||||
sectionId={task.status || 'default'}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[tasks, activeTaskId, overId, onTaskRender]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Memoize the list component to prevent unnecessary re-renders
|
|
||||||
const VirtualizedList = useMemo(
|
|
||||||
() => (
|
|
||||||
<List
|
|
||||||
height={height}
|
|
||||||
width="100%"
|
|
||||||
itemCount={tasks.length}
|
|
||||||
itemSize={itemHeight}
|
|
||||||
itemData={taskData}
|
|
||||||
overscanCount={10} // Increased overscan for smoother scrolling experience
|
|
||||||
className="virtualized-task-list"
|
|
||||||
>
|
|
||||||
{Row}
|
|
||||||
</List>
|
|
||||||
),
|
|
||||||
[height, tasks.length, itemHeight, taskData, Row]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (tasks.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="virtualized-empty-state" style={{ height, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
||||||
<div className="empty-message" style={{
|
|
||||||
padding: '32px 24px',
|
|
||||||
color: '#8c8c8c',
|
|
||||||
fontSize: '14px',
|
|
||||||
backgroundColor: '#fafafa',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #f0f0f0'
|
|
||||||
}}>
|
|
||||||
No tasks in this group
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return VirtualizedList;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default React.memo(VirtualizedTaskList);
|
|
||||||
Reference in New Issue
Block a user