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 alertService from '@/services/alerts/alertService';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import Skeleton from 'antd/es/skeleton/Skeleton';
|
||||
import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status';
|
||||
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
|
||||
|
||||
@@ -148,11 +147,11 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
|
||||
const targetGroup = taskGroups.find(g => g.id === targetGroupId);
|
||||
if (!sourceGroup || !targetGroup) return;
|
||||
|
||||
|
||||
const taskIdx = sourceGroup.tasks.findIndex(t => t.id === draggedTaskId);
|
||||
if (taskIdx === -1) return;
|
||||
|
||||
const movedTask = sourceGroup.tasks[taskIdx];
|
||||
let didStatusChange = false;
|
||||
if (groupBy === 'status' && movedTask.id) {
|
||||
if (sourceGroup.id !== targetGroup.id) {
|
||||
const canContinue = await checkTaskDependencyStatus(movedTask.id, targetGroupId);
|
||||
@@ -163,6 +162,7 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
|
||||
);
|
||||
return;
|
||||
}
|
||||
didStatusChange = true;
|
||||
}
|
||||
}
|
||||
let insertIdx = hoveredTaskIdx;
|
||||
@@ -259,6 +259,18 @@ const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ project
|
||||
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);
|
||||
|
||||
@@ -15,6 +15,7 @@ import { SocketEvents } from '@/shared/socket-events';
|
||||
import { getUserSession } from '@/utils/session-helper';
|
||||
import { themeWiseColor } from '@/utils/themeWiseColor';
|
||||
import { toggleTaskExpansion, fetchBoardSubTasks } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||
import TaskProgressCircle from './TaskProgressCircle';
|
||||
|
||||
// Simple Portal component
|
||||
const Portal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
@@ -69,7 +70,6 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
|
||||
const d = selectedDate || new Date();
|
||||
return new Date(d.getFullYear(), d.getMonth(), 1);
|
||||
});
|
||||
const [showSubtasks, setShowSubtasks] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedDate(task.end_date ? new Date(task.end_date) : null);
|
||||
@@ -202,7 +202,11 @@ const TaskCard: React.FC<TaskCardProps> = memo(({
|
||||
|
||||
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
|
||||
draggable
|
||||
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