Merge pull request #185 from shancds/refact/board-task-card-performance

Refact/board task card performance
This commit is contained in:
Chamika J
2025-06-25 16:25:32 +05:30
committed by GitHub
6 changed files with 191 additions and 52 deletions

View File

@@ -44,6 +44,7 @@ import { ITaskStatusCreateRequest } from '@/types/tasks/task-status-create-reque
import alertService from '@/services/alerts/alertService';
import { IGroupBy } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import EnhancedKanbanCreateSection from './EnhancedKanbanCreateSection';
import ImprovedTaskFilters from '../task-management/improved-task-filters';
// Import the TaskListFilters component
const TaskListFilters = React.lazy(() => import('@/pages/projects/projectView/taskList/task-list-filters/task-list-filters'));
@@ -382,15 +383,12 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
return (
<>
<Card
size="small"
className="mb-4"
styles={{ body: { padding: '12px 16px' } }}
>
{/* Task Filters */}
<div className="mb-4">
<React.Suspense fallback={<div>Loading filters...</div>}>
<TaskListFilters position="board" />
<ImprovedTaskFilters position="board" />
</React.Suspense>
</Card>
</div>
<div className={`enhanced-kanban-board ${className}`}>
{/* Performance Monitor - only show for large datasets */}
{/* {performanceMetrics.totalTasks > 100 && <PerformanceMonitor />} */}
@@ -431,6 +429,7 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
{activeTask && (
<EnhancedKanbanTaskCard
task={activeTask}
sectionId={activeTask.status_id || ''}
isDragOverlay={true}
/>
)}

View File

@@ -121,6 +121,7 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
const renderTask = useMemo(() => (task: any, index: number) => (
<EnhancedKanbanTaskCard
key={task.id}
sectionId={group.id}
task={task}
isActive={task.id === activeTaskId}
isDropTarget={overId === task.id}
@@ -413,7 +414,7 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
type="text"
size="small"
shape="circle"
style={{ color: themeMode === 'dark' ? '#383838' : '' }}
// style={{ color: themeMode === 'dark' ? '#383838' : '' }}
onClick={() => {
setShowNewCardTop(true);
setShowNewCardBottom(false);
@@ -433,8 +434,8 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
<MoreOutlined
style={{
rotate: '90deg',
fontSize: '25px',
color: themeMode === 'dark' ? '#383838' : '',
// fontSize: '25px',
// color: themeMode === 'dark' ? '#383838' : '',
}}
/>
</Button>
@@ -488,6 +489,7 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
<EnhancedKanbanTaskCard
task={task}
sectionId={group.id}
isActive={task.id === activeTaskId}
isDropTarget={overId === task.id}
/>

View File

@@ -20,26 +20,46 @@ import { ForkOutlined } from '@ant-design/icons';
import { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import { CaretDownFilled, CaretRightFilled } from '@ant-design/icons';
import { fetchBoardSubTasks } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { Divider } from 'antd';
import { List } from 'antd';
import { Skeleton } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import BoardSubTaskCard from '@/pages/projects/projectView/board/board-section/board-sub-task-card/board-sub-task-card';
import BoardCreateSubtaskCard from '@/pages/projects/projectView/board/board-section/board-sub-task-card/board-create-sub-task-card';
import { useTranslation } from 'react-i18next';
interface EnhancedKanbanTaskCardProps {
task: IProjectTask;
sectionId: string;
isActive?: boolean;
isDragOverlay?: boolean;
isDropTarget?: boolean;
}
// Priority and status colors - moved outside component to avoid recreation
const PRIORITY_COLORS = {
critical: '#ff4d4f',
high: '#ff7a45',
medium: '#faad14',
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 [isSubTaskShow, setIsSubTaskShow] = useState(false);
const projectId = useAppSelector(state => state.projectReducer.projectId);
const {
attributes,
listeners,
@@ -70,14 +90,8 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
// Don't handle click if we're dragging
if (isDragging) return;
// Add a small delay to ensure it's a click and not the start of a drag
const clickTimeout = setTimeout(() => {
dispatch(setSelectedTaskId(id));
dispatch(setShowTaskDrawer(true));
}, 50);
return () => clearTimeout(clickTimeout);
}, [dispatch, isDragging]);
const renderLabels = useMemo(() => {
@@ -97,6 +111,32 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
);
}, [task.labels, themeMode]);
const handleSubTaskExpand = useCallback(() => {
console.log('handleSubTaskExpand', task, projectId);
if (task && task.id && projectId) {
if (task.show_sub_tasks) {
// If subtasks are already loaded, just toggle visibility
setIsSubTaskShow(prev => !prev);
} else {
// If subtasks need to be fetched, show the section first with loading state
setIsSubTaskShow(true);
dispatch(fetchBoardSubTasks({ taskId: task.id, projectId }));
}
}
}, [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}
@@ -117,7 +157,10 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
</Flex>
<Flex gap={4} align="center">
{/* Action Icons */}
<PrioritySection task={task} />
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: task.priority_color || '#d9d9d9' }}
/>
<Typography.Text
style={{ fontWeight: 500 }}
ellipsis={{ tooltip: task.name }}
@@ -132,14 +175,14 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
marginBlock: 8,
}}
>
{task && <CustomAvatarGroup task={task} sectionId={task.status_id || ''} />}
{task && <CustomAvatarGroup task={task} sectionId={sectionId} />}
<Flex gap={4} align="center">
<CustomDueDatePicker task={task} onDateChange={setDueDate} />
{/* Subtask Section */}
<Button
// onClick={handleSubtaskButtonClick}
onClick={handleSubtaskButtonClick}
size="small"
style={{
padding: 0,
@@ -162,6 +205,45 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
</Button>
</Flex>
</Flex>
<Flex vertical gap={8}>
{isSubTaskShow && (
<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>
)}
{!task.sub_tasks_loading && task?.sub_tasks &&
task?.sub_tasks.map((subtask: any) => (
<BoardSubTaskCard key={subtask.id} subtask={subtask} sectionId={sectionId} />
))}
{showNewSubtaskCard && (
<BoardCreateSubtaskCard
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>
);

View File

@@ -38,7 +38,7 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = ({
onTaskRender?.(task, index);
return (
<div style={style} className="virtualized-task-row">
<div className="virtualized-task-row">
<EnhancedKanbanTaskCard
task={task}
isActive={task.id === activeTaskId}

View File

@@ -263,6 +263,60 @@ export const reorderEnhancedKanbanGroups = createAsyncThunk(
}
);
export const fetchBoardSubTasks = createAsyncThunk(
'enhancedKanban/fetchBoardSubTasks',
async (
{ taskId, projectId }: { taskId: string; projectId: string },
{ rejectWithValue, getState }
) => {
try {
const state = getState() as { enhancedKanbanReducer: EnhancedKanbanState };
const { enhancedKanbanReducer } = state;
// Check if the task is already expanded (optional, can be enhanced later)
// const task = enhancedKanbanReducer.taskGroups.flatMap(group => group.tasks).find(t => t.id === taskId);
// if (task?.show_sub_tasks) {
// return [];
// }
const selectedMembers = enhancedKanbanReducer.taskAssignees
.filter(member => member.selected)
.map(member => member.id)
.join(' ');
const selectedLabels = enhancedKanbanReducer.labels
.filter(label => label.selected)
.map(label => label.id)
.join(' ');
const config: ITaskListConfigV2 = {
id: projectId,
archived: enhancedKanbanReducer.archived,
group: enhancedKanbanReducer.groupBy,
field: enhancedKanbanReducer.fields.map(field => `${field.key} ${field.sort_order}`).join(','),
order: '',
search: enhancedKanbanReducer.search || '',
statuses: '',
members: selectedMembers,
projects: '',
isSubtasksInclude: false,
labels: selectedLabels,
priorities: enhancedKanbanReducer.priorities.join(' '),
parent_task: taskId,
};
const response = await tasksApiService.getTaskList(config);
return response.body;
} catch (error) {
logger.error('Fetch Enhanced Board Sub Tasks', error);
if (error instanceof Error) {
return rejectWithValue(error.message);
}
return rejectWithValue('Failed to fetch sub tasks');
}
}
);
const enhancedKanbanSlice = createSlice({
name: 'enhancedKanbanReducer',
initialState,

View File

@@ -53,6 +53,7 @@ import useIsProjectManager from '@/hooks/useIsProjectManager';
import useTabSearchParam from '@/hooks/useTabSearchParam';
import { addTaskCardToTheTop, fetchBoardTaskGroups } from '@/features/board/board-slice';
import { fetchPhasesByProjectId } from '@/features/projects/singleProject/phase/phases.slice';
import { fetchEnhancedKanbanGroups } from '@/features/enhanced-kanban/enhanced-kanban.slice';
const ProjectViewHeader = () => {
const navigate = useNavigate();
@@ -84,7 +85,8 @@ const ProjectViewHeader = () => {
dispatch(fetchTaskGroups(projectId));
break;
case 'board':
dispatch(fetchBoardTaskGroups(projectId));
// dispatch(fetchBoardTaskGroups(projectId));
dispatch(fetchEnhancedKanbanGroups(projectId));
break;
case 'project-insights-member-overview':
dispatch(setRefreshTimestamp());