expand sub tasks

This commit is contained in:
chamiakJ
2025-07-03 01:31:05 +05:30
parent 3bef18901a
commit ecd4d29a38
435 changed files with 13150 additions and 11087 deletions

View File

@@ -40,4 +40,4 @@
justify-content: center;
align-items: center;
min-height: 200px;
}
}

View File

@@ -16,10 +16,7 @@ import {
pointerWithin,
rectIntersection,
} from '@dnd-kit/core';
import {
SortableContext,
horizontalListSortingStrategy,
} from '@dnd-kit/sortable';
import { SortableContext, horizontalListSortingStrategy } from '@dnd-kit/sortable';
import { RootState } from '@/app/store';
import {
fetchEnhancedKanbanGroups,
@@ -49,7 +46,9 @@ import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
import { useAuthService } from '@/hooks/useAuth';
// Import the TaskListFilters component
const TaskListFilters = React.lazy(() => import('@/pages/projects/projectView/taskList/task-list-filters/task-list-filters'));
const TaskListFilters = React.lazy(
() => import('@/pages/projects/projectView/taskList/task-list-filters/task-list-filters')
);
interface EnhancedKanbanBoardProps {
projectId: string;
className?: string;
@@ -57,24 +56,22 @@ interface EnhancedKanbanBoardProps {
const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, className = '' }) => {
const dispatch = useDispatch();
const {
taskGroups,
loadingGroups,
error,
dragState,
performanceMetrics
} = useSelector((state: RootState) => state.enhancedKanbanReducer);
const { taskGroups, loadingGroups, error, dragState, performanceMetrics } = useSelector(
(state: RootState) => state.enhancedKanbanReducer
);
const { socket } = useSocket();
const authService = useAuthService();
const teamId = authService.getCurrentSession()?.team_id;
const groupBy = useSelector((state: RootState) => state.enhancedKanbanReducer.groupBy);
const project = useAppSelector((state: RootState) => state.projectReducer.project);
const { statusCategories, status: existingStatuses } = useAppSelector((state) => state.taskStatusReducer);
const { statusCategories, status: existingStatuses } = useAppSelector(
state => state.taskStatusReducer
);
const themeMode = useAppSelector(state => state.themeReducer.mode);
// Load filter data
useFilterDataLoader();
// Set up socket event handlers for real-time updates
useTaskSocketHandlers();
@@ -106,22 +103,18 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
}, [dispatch, projectId]);
// Get all task IDs for sortable context
const allTaskIds = useMemo(() =>
taskGroups.flatMap(group => group.tasks.map(task => task.id!)),
[taskGroups]
);
const allGroupIds = useMemo(() =>
taskGroups.map(group => group.id),
const allTaskIds = useMemo(
() => taskGroups.flatMap(group => group.tasks.map(task => task.id!)),
[taskGroups]
);
const allGroupIds = useMemo(() => taskGroups.map(group => group.id), [taskGroups]);
// Enhanced collision detection
const collisionDetectionStrategy = (args: any) => {
// First, let's see if we're colliding with any droppable areas
const pointerIntersections = pointerWithin(args);
const intersections = pointerIntersections.length > 0
? pointerIntersections
: rectIntersection(args);
const intersections =
pointerIntersections.length > 0 ? pointerIntersections : rectIntersection(args);
let overId = getFirstCollision(intersections, 'id');
@@ -162,11 +155,13 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
setActiveGroup(foundGroup);
setActiveTask(null);
dispatch(setDragState({
activeTaskId: null,
activeGroupId: activeId,
isDragging: true,
}));
dispatch(
setDragState({
activeTaskId: null,
activeGroupId: activeId,
isDragging: true,
})
);
} else {
// Dragging a task
let foundTask = null;
@@ -184,11 +179,13 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
setActiveTask(foundTask);
setActiveGroup(null);
dispatch(setDragState({
activeTaskId: activeId,
activeGroupId: foundGroup?.id || null,
isDragging: true,
}));
dispatch(
setDragState({
activeTaskId: activeId,
activeGroupId: foundGroup?.id || null,
isDragging: true,
})
);
}
};
@@ -220,12 +217,14 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
setOverId(null);
// Reset Redux drag state
dispatch(setDragState({
activeTaskId: null,
activeGroupId: null,
overId: null,
isDragging: false,
}));
dispatch(
setDragState({
activeTaskId: null,
activeGroupId: null,
overId: null,
isDragging: false,
})
);
if (!over) return;
@@ -258,7 +257,7 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
// Call API to update status order
try {
const requestBody: ITaskStatusCreateRequest = {
status_order: columnOrder
status_order: columnOrder,
};
const response = await statusApiService.updateStatusOrder(requestBody, projectId);
@@ -267,7 +266,13 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
const revertedGroups = [...reorderedGroups];
const [movedBackGroup] = revertedGroups.splice(toIndex, 1);
revertedGroups.splice(fromIndex, 0, movedBackGroup);
dispatch(reorderGroups({ fromIndex: toIndex, toIndex: fromIndex, reorderedGroups: revertedGroups }));
dispatch(
reorderGroups({
fromIndex: toIndex,
toIndex: fromIndex,
reorderedGroups: revertedGroups,
})
);
alertService.error('Failed to update column order', 'Please try again');
}
} catch (error) {
@@ -275,7 +280,13 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
const revertedGroups = [...reorderedGroups];
const [movedBackGroup] = revertedGroups.splice(toIndex, 1);
revertedGroups.splice(fromIndex, 0, movedBackGroup);
dispatch(reorderGroups({ fromIndex: toIndex, toIndex: fromIndex, reorderedGroups: revertedGroups }));
dispatch(
reorderGroups({
fromIndex: toIndex,
toIndex: fromIndex,
reorderedGroups: revertedGroups,
})
);
alertService.error('Failed to update column order', 'Please try again');
logger.error('Failed to update column order', error);
}
@@ -338,24 +349,28 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
}
// Synchronous UI update
dispatch(reorderTasks({
activeGroupId: sourceGroup.id,
overGroupId: targetGroup.id,
fromIndex: sourceIndex,
toIndex: targetIndex,
task: movedTask,
updatedSourceTasks,
updatedTargetTasks,
}));
dispatch(reorderEnhancedKanbanTasks({
activeGroupId: sourceGroup.id,
overGroupId: targetGroup.id,
fromIndex: sourceIndex,
toIndex: targetIndex,
task: movedTask,
updatedSourceTasks,
updatedTargetTasks,
}) as any);
dispatch(
reorderTasks({
activeGroupId: sourceGroup.id,
overGroupId: targetGroup.id,
fromIndex: sourceIndex,
toIndex: targetIndex,
task: movedTask,
updatedSourceTasks,
updatedTargetTasks,
})
);
dispatch(
reorderEnhancedKanbanTasks({
activeGroupId: sourceGroup.id,
overGroupId: targetGroup.id,
fromIndex: sourceIndex,
toIndex: targetIndex,
task: movedTask,
updatedSourceTasks,
updatedTargetTasks,
}) as any
);
// --- Socket emit for task sort order ---
if (socket && projectId && movedTask) {
@@ -368,7 +383,10 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
toSortOrder = -1;
toLastIndex = true;
} else if (targetGroup.tasks[targetIndex]) {
toSortOrder = typeof targetGroup.tasks[targetIndex].sort_order === 'number' ? targetGroup.tasks[targetIndex].sort_order! : -1;
toSortOrder =
typeof targetGroup.tasks[targetIndex].sort_order === 'number'
? targetGroup.tasks[targetIndex].sort_order!
: -1;
toLastIndex = false;
} else if (targetGroup.tasks.length > 0) {
const lastSortOrder = targetGroup.tasks[targetGroup.tasks.length - 1].sort_order;
@@ -490,4 +508,4 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
);
};
export default EnhancedKanbanBoard;
export default EnhancedKanbanBoard;

View File

@@ -7,7 +7,10 @@ import { nanoid } from '@reduxjs/toolkit';
import { useAppSelector } from '@/hooks/useAppSelector';
import { themeWiseColor } from '@/utils/themeWiseColor';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { IGroupBy, fetchEnhancedKanbanGroups } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import {
IGroupBy,
fetchEnhancedKanbanGroups,
} from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { statusApiService } from '@/api/taskAttributes/status/status.api.service';
import { createStatus, fetchStatuses } from '@/features/taskAttributes/taskStatusSlice';
import { ALPHA_CHANNEL } from '@/shared/constants';
@@ -19,10 +22,12 @@ import useIsProjectManager from '@/hooks/useIsProjectManager';
const EnhancedKanbanCreateSection: React.FC = () => {
const { t } = useTranslation('kanban-board');
const themeMode = useAppSelector((state) => state.themeReducer.mode);
const { projectId } = useAppSelector((state) => state.projectReducer);
const groupBy = useAppSelector((state) => state.enhancedKanbanReducer.groupBy);
const { statusCategories, status: existingStatuses } = useAppSelector((state) => state.taskStatusReducer);
const themeMode = useAppSelector(state => state.themeReducer.mode);
const { projectId } = useAppSelector(state => state.projectReducer);
const groupBy = useAppSelector(state => state.enhancedKanbanReducer.groupBy);
const { statusCategories, status: existingStatuses } = useAppSelector(
state => state.taskStatusReducer
);
const dispatch = useAppDispatch();
const isOwnerorAdmin = useAuthService().isOwnerOrAdmin();
@@ -36,20 +41,20 @@ const EnhancedKanbanCreateSection: React.FC = () => {
const getUniqueSectionName = (baseName: string): string => {
// Check if the base name already exists
const existingNames = existingStatuses.map(status => status.name?.toLowerCase());
if (!existingNames.includes(baseName.toLowerCase())) {
return baseName;
}
// If the base name exists, add a number suffix
let counter = 1;
let newName = `${baseName.trim()} (${counter})`;
while (existingNames.includes(newName.toLowerCase())) {
counter++;
newName = `${baseName.trim()} (${counter})`;
}
return newName;
};
@@ -57,14 +62,14 @@ const EnhancedKanbanCreateSection: React.FC = () => {
const sectionId = nanoid();
const baseNameSection = 'Untitled section';
const sectionName = getUniqueSectionName(baseNameSection);
if (groupBy === IGroupBy.STATUS && projectId) {
// Find the "To do" category
const todoCategory = statusCategories.find(category =>
category.name?.toLowerCase() === 'to do' ||
category.name?.toLowerCase() === 'todo'
const todoCategory = statusCategories.find(
category =>
category.name?.toLowerCase() === 'to do' || category.name?.toLowerCase() === 'todo'
);
if (todoCategory && todoCategory.id) {
// Create a new status
const body = {
@@ -72,11 +77,13 @@ const EnhancedKanbanCreateSection: React.FC = () => {
project_id: projectId,
category_id: todoCategory.id,
};
try {
// Create the status
const response = await dispatch(createStatus({ body, currentProjectId: projectId })).unwrap();
const response = await dispatch(
createStatus({ body, currentProjectId: projectId })
).unwrap();
if (response.done && response.body) {
// Refresh the board to show the new section
dispatch(fetchEnhancedKanbanGroups(projectId));
@@ -87,7 +94,7 @@ const EnhancedKanbanCreateSection: React.FC = () => {
logger.error('Failed to create status:', error);
}
}
}
}
if (groupBy === IGroupBy.PHASE && projectId) {
const body = {
@@ -95,7 +102,7 @@ const EnhancedKanbanCreateSection: React.FC = () => {
project_id: projectId,
};
try {
try {
const response = await phasesApiService.addPhaseOption(projectId);
if (response.done && response.body) {
dispatch(fetchEnhancedKanbanGroups(projectId));
@@ -147,4 +154,4 @@ const EnhancedKanbanCreateSection: React.FC = () => {
);
};
export default React.memo(EnhancedKanbanCreateSection);
export default React.memo(EnhancedKanbanCreateSection);

View File

@@ -85,22 +85,27 @@ const EnhancedKanbanCreateSubtaskCard = ({
}, 0);
if (task.parent_task_id) {
socket?.emit(SocketEvents.GET_TASK_PROGRESS.toString(), task.parent_task_id);
socket?.once(SocketEvents.GET_TASK_PROGRESS.toString(), (data: {
id: string;
complete_ratio: number;
completed_count: number;
total_tasks_count: number;
parent_task: string;
}) => {
if (!data.parent_task) data.parent_task = task.parent_task_id || '';
dispatch(updateEnhancedKanbanTaskProgress({
id: task.id || '',
complete_ratio: data.complete_ratio,
completed_count: data.completed_count,
total_tasks_count: data.total_tasks_count,
parent_task: data.parent_task,
}));
});
socket?.once(
SocketEvents.GET_TASK_PROGRESS.toString(),
(data: {
id: string;
complete_ratio: number;
completed_count: number;
total_tasks_count: number;
parent_task: string;
}) => {
if (!data.parent_task) data.parent_task = task.parent_task_id || '';
dispatch(
updateEnhancedKanbanTaskProgress({
id: task.id || '',
complete_ratio: data.complete_ratio,
completed_count: data.completed_count,
total_tasks_count: data.total_tasks_count,
parent_task: data.parent_task,
})
);
}
);
}
});
} catch (error) {
@@ -143,7 +148,7 @@ const EnhancedKanbanCreateSubtaskCard = ({
cursor: 'pointer',
overflow: 'hidden',
}}
// className={`outline-1 ${themeWiseColor('outline-[#edeae9]', 'outline-[#6a696a]', themeMode)} hover:outline`}
// className={`outline-1 ${themeWiseColor('outline-[#edeae9]', 'outline-[#6a696a]', themeMode)} hover:outline`}
onBlur={handleCancelNewCard}
>
<Input
@@ -170,4 +175,4 @@ const EnhancedKanbanCreateSubtaskCard = ({
);
};
export default EnhancedKanbanCreateSubtaskCard;
export default EnhancedKanbanCreateSubtaskCard;

View File

@@ -89,7 +89,12 @@ const EnhancedKanbanCreateTaskCard: React.FC<EnhancedKanbanCreateTaskCardProps>
// Real-time socket event handler
const eventHandler = (task: IProjectTask) => {
dispatch(addTaskToGroup({ sectionId, task: { ...task, id: task.id || nanoid(), name: task.name || newTaskName.trim() } }));
dispatch(
addTaskToGroup({
sectionId,
task: { ...task, id: task.id || nanoid(), name: task.name || newTaskName.trim() },
})
);
socket?.off(SocketEvents.QUICK_TASK.toString(), eventHandler);
resetForNextTask();
};
@@ -159,4 +164,4 @@ const EnhancedKanbanCreateTaskCard: React.FC<EnhancedKanbanCreateTaskCardProps>
);
};
export default EnhancedKanbanCreateTaskCard;
export default EnhancedKanbanCreateTaskCard;

View File

@@ -127,7 +127,8 @@
}
@keyframes dropPulse {
0%, 100% {
0%,
100% {
opacity: 0.6;
transform: scaleX(0.8);
}
@@ -205,7 +206,7 @@
min-width: 240px;
max-width: 280px;
}
.enhanced-kanban-group-tasks {
max-height: 400px;
}
@@ -216,7 +217,7 @@
min-width: 200px;
max-width: 240px;
}
.enhanced-kanban-group-tasks {
max-height: 300px;
}
@@ -239,4 +240,4 @@
max-width: 220px;
display: inline-block;
vertical-align: middle;
}
}

View File

@@ -47,7 +47,7 @@
}
.enhanced-kanban-task-card.drop-target::before {
content: '';
content: "";
position: absolute;
top: -2px;
left: -2px;
@@ -60,7 +60,8 @@
}
@keyframes dropTargetPulse {
0%, 100% {
0%,
100% {
opacity: 0.3;
transform: scale(1);
}
@@ -117,4 +118,4 @@
font-size: 12px;
color: var(--ant-color-text-tertiary);
margin-top: 4px;
}
}

View File

@@ -19,7 +19,10 @@ import { ForkOutlined } from '@ant-design/icons';
import { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import { CaretDownFilled, CaretRightFilled } from '@ant-design/icons';
import { fetchBoardSubTasks, toggleTaskExpansion } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import {
fetchBoardSubTasks,
toggleTaskExpansion,
} from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { Divider } from 'antd';
import { List } from 'antd';
import { Skeleton } from 'antd';
@@ -46,227 +49,233 @@ const PRIORITY_COLORS = {
low: '#52c41a',
} as const;
const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo(({
task,
sectionId,
isActive = false,
isDragOverlay = false,
isDropTarget = false
}) => {
const dispatch = useAppDispatch();
const { t } = useTranslation('kanban-board');
const themeMode = useAppSelector(state => state.themeReducer.mode);
const [showNewSubtaskCard, setShowNewSubtaskCard] = useState(false);
const [dueDate, setDueDate] = useState<Dayjs | null>(
task?.end_date ? dayjs(task?.end_date) : null
);
const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo(
({ task, sectionId, isActive = false, isDragOverlay = false, isDropTarget = false }) => {
const dispatch = useAppDispatch();
const { t } = useTranslation('kanban-board');
const themeMode = useAppSelector(state => state.themeReducer.mode);
const [showNewSubtaskCard, setShowNewSubtaskCard] = useState(false);
const [dueDate, setDueDate] = useState<Dayjs | null>(
task?.end_date ? dayjs(task?.end_date) : null
);
const projectId = useAppSelector(state => state.projectReducer.projectId);
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id: task.id!,
data: {
type: 'task',
task,
},
disabled: isDragOverlay,
animateLayoutChanges: defaultAnimateLayoutChanges,
});
const projectId = useAppSelector(state => state.projectReducer.projectId);
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: task.id!,
data: {
type: 'task',
task,
},
disabled: isDragOverlay,
animateLayoutChanges: defaultAnimateLayoutChanges,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
backgroundColor: themeMode === 'dark' ? '#292929' : '#fafafa',
};
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
backgroundColor: themeMode === 'dark' ? '#292929' : '#fafafa',
};
const handleCardClick = useCallback((e: React.MouseEvent, id: string) => {
// Prevent the event from propagating to parent elements
e.stopPropagation();
const handleCardClick = useCallback(
(e: React.MouseEvent, id: string) => {
// Prevent the event from propagating to parent elements
e.stopPropagation();
// Don't handle click if we're dragging
if (isDragging) return;
dispatch(setSelectedTaskId(id));
dispatch(setShowTaskDrawer(true));
}, [dispatch, isDragging]);
// Don't handle click if we're dragging
if (isDragging) return;
dispatch(setSelectedTaskId(id));
dispatch(setShowTaskDrawer(true));
},
[dispatch, isDragging]
);
const renderLabels = useMemo(() => {
if (!task?.labels?.length) return null;
const renderLabels = useMemo(() => {
if (!task?.labels?.length) return null;
return (
<>
{task.labels.slice(0, 2).map((label: any) => (
<Tag key={label.id} style={{ marginRight: '2px' }} color={label?.color_code}>
<span style={{ color: themeMode === 'dark' ? '#383838' : '', fontSize: 10 }}>
{label.name}
</span>
</Tag>
))}
{task.labels.length > 2 && <Tag>+ {task.labels.length - 2}</Tag>}
</>
);
}, [task.labels, themeMode]);
const handleSubTaskExpand = useCallback(() => {
if (task && task.id && projectId) {
// Check if subtasks are already loaded and we have subtask data
if (task.sub_tasks && task.sub_tasks.length > 0 && task.sub_tasks_count > 0) {
// If subtasks are already loaded, just toggle visibility
dispatch(toggleTaskExpansion(task.id));
} else if (task.sub_tasks_count > 0) {
// If we have a subtask count but no loaded subtasks, fetch them
dispatch(toggleTaskExpansion(task.id));
dispatch(fetchBoardSubTasks({ taskId: task.id, projectId }));
} else {
// If no subtasks exist, just toggle visibility (will show empty state)
dispatch(toggleTaskExpansion(task.id));
}
}
}, [task, projectId, dispatch]);
const handleSubtaskButtonClick = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
handleSubTaskExpand();
},
[handleSubTaskExpand]
);
const handleAddSubtaskClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
setShowNewSubtaskCard(true);
}, []);
return (
<>
{task.labels.slice(0, 2).map((label: any) => (
<Tag key={label.id} style={{ marginRight: '2px' }} color={label?.color_code}>
<span style={{ color: themeMode === 'dark' ? '#383838' : '', fontSize: 10 }}>
{label.name}
</span>
</Tag>
))}
{task.labels.length > 2 && <Tag>+ {task.labels.length - 2}</Tag>}
</>
);
}, [task.labels, themeMode]);
<div
ref={setNodeRef}
style={style}
className={`enhanced-kanban-task-card ${isActive ? 'active' : ''} ${isDragging ? 'dragging' : ''} ${isDragOverlay ? 'drag-overlay' : ''} ${isDropTarget ? 'drop-target' : ''}`}
{...attributes}
{...listeners}
>
<div className="task-content" onClick={e => handleCardClick(e, task.id || '')}>
<Flex align="center" justify="space-between" className="mb-2">
<Flex>{renderLabels}</Flex>
const handleSubTaskExpand = useCallback(() => {
if (task && task.id && projectId) {
// Check if subtasks are already loaded and we have subtask data
if (task.sub_tasks && task.sub_tasks.length > 0 && task.sub_tasks_count > 0) {
// If subtasks are already loaded, just toggle visibility
dispatch(toggleTaskExpansion(task.id));
} else if (task.sub_tasks_count > 0) {
// If we have a subtask count but no loaded subtasks, fetch them
dispatch(toggleTaskExpansion(task.id));
dispatch(fetchBoardSubTasks({ taskId: task.id, projectId }));
} else {
// If no subtasks exist, just toggle visibility (will show empty state)
dispatch(toggleTaskExpansion(task.id));
}
}
}, [task, projectId, dispatch]);
const handleSubtaskButtonClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
handleSubTaskExpand();
}, [handleSubTaskExpand]);
const handleAddSubtaskClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
setShowNewSubtaskCard(true);
}, []);
return (
<div
ref={setNodeRef}
style={style}
className={`enhanced-kanban-task-card ${isActive ? 'active' : ''} ${isDragging ? 'dragging' : ''} ${isDragOverlay ? 'drag-overlay' : ''} ${isDropTarget ? 'drop-target' : ''}`}
{...attributes}
{...listeners}
>
<div className="task-content" onClick={e => handleCardClick(e, task.id || '')}>
<Flex align="center" justify="space-between" className="mb-2">
<Flex>
{renderLabels}
</Flex>
<Tooltip title={` ${task?.completed_count} / ${task?.total_tasks_count}`}>
<Progress type="circle" percent={task?.complete_ratio} size={24} strokeWidth={(task.complete_ratio || 0) >= 100 ? 9 : 7} />
</Tooltip>
</Flex>
<Flex gap={4} align="center">
{/* Action Icons */}
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: task.priority_color || '#d9d9d9' }}
/>
<Typography.Text
style={{ fontWeight: 500 }}
ellipsis={{ tooltip: task.name }}
>
{task.name}
</Typography.Text>
</Flex>
<Flex
align="center"
justify="space-between"
style={{
marginBlock: 8,
}}
>
<Flex align="center" gap={2}>
<AvatarGroup
members={task.names || []}
maxCount={3}
isDarkMode={themeMode === 'dark'}
size={24}
/>
<LazyAssigneeSelectorWrapper task={task} groupId={sectionId} isDarkMode={themeMode === 'dark'} />
<Tooltip title={` ${task?.completed_count} / ${task?.total_tasks_count}`}>
<Progress
type="circle"
percent={task?.complete_ratio}
size={24}
strokeWidth={(task.complete_ratio || 0) >= 100 ? 9 : 7}
/>
</Tooltip>
</Flex>
<Flex gap={4} align="center">
<CustomDueDatePicker task={task} onDateChange={setDueDate} />
{/* Subtask Section */}
<Button
onClick={handleSubtaskButtonClick}
size="small"
style={{
padding: 0,
}}
type="text"
>
<Tag
bordered={false}
style={{
display: 'flex',
alignItems: 'center',
margin: 0,
backgroundColor: themeWiseColor('white', '#1e1e1e', themeMode),
}}
>
<ForkOutlined rotate={90} />
<span>{task.sub_tasks_count}</span>
{task.show_sub_tasks ? <CaretDownFilled /> : <CaretRightFilled />}
</Tag>
</Button>
{/* Action Icons */}
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: task.priority_color || '#d9d9d9' }}
/>
<Typography.Text style={{ fontWeight: 500 }} ellipsis={{ tooltip: task.name }}>
{task.name}
</Typography.Text>
</Flex>
</Flex>
<Flex vertical gap={8}>
{task.show_sub_tasks && (
<Flex vertical>
<Divider style={{ marginBlock: 0 }} />
<List>
{task.sub_tasks_loading && (
<List.Item>
<Skeleton active paragraph={{ rows: 2 }} title={false} style={{ marginTop: 8 }} />
</List.Item>
)}
<Flex
align="center"
justify="space-between"
style={{
marginBlock: 8,
}}
>
<Flex align="center" gap={2}>
<AvatarGroup
members={task.names || []}
maxCount={3}
isDarkMode={themeMode === 'dark'}
size={24}
/>
<LazyAssigneeSelectorWrapper
task={task}
groupId={sectionId}
isDarkMode={themeMode === 'dark'}
/>
</Flex>
<Flex gap={4} align="center">
<CustomDueDatePicker task={task} onDateChange={setDueDate} />
{!task.sub_tasks_loading && task?.sub_tasks && task.sub_tasks.length > 0 &&
task.sub_tasks.map((subtask: any) => (
<BoardSubTaskCard key={subtask.id} subtask={subtask} sectionId={sectionId} />
))}
{!task.sub_tasks_loading && (!task?.sub_tasks || task.sub_tasks.length === 0) && task.sub_tasks_count === 0 && (
<List.Item>
<div style={{ padding: '8px 0', color: '#999', fontSize: '12px' }}>
{t('noSubtasks', 'No subtasks')}
</div>
</List.Item>
)}
{showNewSubtaskCard && (
<EnhancedKanbanCreateSubtaskCard
sectionId={sectionId}
parentTaskId={task.id || ''}
setShowNewSubtaskCard={setShowNewSubtaskCard}
/>
)}
</List>
{/* Subtask Section */}
<Button
type="text"
onClick={handleSubtaskButtonClick}
size="small"
style={{
width: 'fit-content',
borderRadius: 6,
boxShadow: 'none',
padding: 0,
}}
icon={<PlusOutlined />}
onClick={handleAddSubtaskClick}
type="text"
>
{t('addSubtask', 'Add Subtask')}
<Tag
bordered={false}
style={{
display: 'flex',
alignItems: 'center',
margin: 0,
backgroundColor: themeWiseColor('white', '#1e1e1e', themeMode),
}}
>
<ForkOutlined rotate={90} />
<span>{task.sub_tasks_count}</span>
{task.show_sub_tasks ? <CaretDownFilled /> : <CaretRightFilled />}
</Tag>
</Button>
</Flex>
)}
</Flex>
</div>
</div>
);
});
</Flex>
<Flex vertical gap={8}>
{task.show_sub_tasks && (
<Flex vertical>
<Divider style={{ marginBlock: 0 }} />
<List>
{task.sub_tasks_loading && (
<List.Item>
<Skeleton
active
paragraph={{ rows: 2 }}
title={false}
style={{ marginTop: 8 }}
/>
</List.Item>
)}
export default EnhancedKanbanTaskCard;
{!task.sub_tasks_loading &&
task?.sub_tasks &&
task.sub_tasks.length > 0 &&
task.sub_tasks.map((subtask: any) => (
<BoardSubTaskCard key={subtask.id} subtask={subtask} sectionId={sectionId} />
))}
{!task.sub_tasks_loading &&
(!task?.sub_tasks || task.sub_tasks.length === 0) &&
task.sub_tasks_count === 0 && (
<List.Item>
<div style={{ padding: '8px 0', color: '#999', fontSize: '12px' }}>
{t('noSubtasks', 'No subtasks')}
</div>
</List.Item>
)}
{showNewSubtaskCard && (
<EnhancedKanbanCreateSubtaskCard
sectionId={sectionId}
parentTaskId={task.id || ''}
setShowNewSubtaskCard={setShowNewSubtaskCard}
/>
)}
</List>
<Button
type="text"
style={{
width: 'fit-content',
borderRadius: 6,
boxShadow: 'none',
}}
icon={<PlusOutlined />}
onClick={handleAddSubtaskClick}
>
{t('addSubtask', 'Add Subtask')}
</Button>
</Flex>
)}
</Flex>
</div>
</div>
);
}
);
export default EnhancedKanbanTaskCard;

View File

@@ -93,9 +93,9 @@
width: 100%;
margin-bottom: 16px;
}
.performance-metrics {
grid-template-columns: 1fr;
gap: 8px;
}
}
}

View File

@@ -21,11 +21,16 @@ const PerformanceMonitor: React.FC = () => {
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';
case 'critical':
return 'red';
case 'warning':
return 'orange';
case 'good':
return 'blue';
case 'excellent':
return 'green';
default:
return 'default';
}
};
@@ -33,15 +38,15 @@ const PerformanceMonitor: React.FC = () => {
const statusColor = getStatusColor(status);
return (
<Card
size="small"
<Card
size="small"
className="performance-monitor"
title={
<div className="performance-monitor-header">
<span>Performance Monitor</span>
<Badge
status={statusColor as any}
text={status.toUpperCase()}
<Badge
status={statusColor as any}
text={status.toUpperCase()}
className="performance-status"
/>
</div>
@@ -56,7 +61,7 @@ const PerformanceMonitor: React.FC = () => {
valueStyle={{ fontSize: '16px' }}
/>
</Tooltip>
<Tooltip title="Largest group by number of tasks">
<Statistic
title="Largest Group"
@@ -65,7 +70,7 @@ const PerformanceMonitor: React.FC = () => {
valueStyle={{ fontSize: '16px' }}
/>
</Tooltip>
<Tooltip title="Average tasks per group">
<Statistic
title="Average Group"
@@ -74,18 +79,18 @@ const PerformanceMonitor: React.FC = () => {
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'}
<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>
@@ -100,4 +105,4 @@ const PerformanceMonitor: React.FC = () => {
);
};
export default React.memo(PerformanceMonitor);
export default React.memo(PerformanceMonitor);

View File

@@ -57,4 +57,4 @@
.virtualized-task-list::-webkit-scrollbar-thumb:hover {
background: var(--ant-color-text-tertiary);
}
}

View File

@@ -22,45 +22,54 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = ({
onTaskRender,
}) => {
// Memoize task data to prevent unnecessary re-renders
const taskData = useMemo(() => ({
tasks,
activeTaskId,
overId,
onTaskRender,
}), [tasks, activeTaskId, overId, onTaskRender]);
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;
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);
// Call onTaskRender callback if provided
onTaskRender?.(task, index);
return (
return (
<EnhancedKanbanTaskCard
task={task}
isActive={task.id === activeTaskId}
isDropTarget={overId === task.id}
sectionId={task.status || 'default'}
/>
);
}, [tasks, activeTaskId, overId, onTaskRender]);
);
},
[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]);
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 (
@@ -73,4 +82,4 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = ({
return VirtualizedList;
};
export default React.memo(VirtualizedTaskList);
export default React.memo(VirtualizedTaskList);