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:
Chamika J
2025-07-15 14:24:24 +05:30
committed by GitHub
7 changed files with 62 additions and 367 deletions

View File

@@ -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);

View File

@@ -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)}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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);