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

View File

@@ -121,6 +121,7 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
const renderTask = useMemo(() => (task: any, index: number) => ( const renderTask = useMemo(() => (task: any, index: number) => (
<EnhancedKanbanTaskCard <EnhancedKanbanTaskCard
key={task.id} key={task.id}
sectionId={group.id}
task={task} task={task}
isActive={task.id === activeTaskId} isActive={task.id === activeTaskId}
isDropTarget={overId === task.id} isDropTarget={overId === task.id}
@@ -413,7 +414,7 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
type="text" type="text"
size="small" size="small"
shape="circle" shape="circle"
style={{ color: themeMode === 'dark' ? '#383838' : '' }} // style={{ color: themeMode === 'dark' ? '#383838' : '' }}
onClick={() => { onClick={() => {
setShowNewCardTop(true); setShowNewCardTop(true);
setShowNewCardBottom(false); setShowNewCardBottom(false);
@@ -433,8 +434,8 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
<MoreOutlined <MoreOutlined
style={{ style={{
rotate: '90deg', rotate: '90deg',
fontSize: '25px', // fontSize: '25px',
color: themeMode === 'dark' ? '#383838' : '', // color: themeMode === 'dark' ? '#383838' : '',
}} }}
/> />
</Button> </Button>
@@ -488,6 +489,7 @@ const EnhancedKanbanGroup: React.FC<EnhancedKanbanGroupProps> = React.memo(({
<EnhancedKanbanTaskCard <EnhancedKanbanTaskCard
task={task} task={task}
sectionId={group.id}
isActive={task.id === activeTaskId} isActive={task.id === activeTaskId}
isDropTarget={overId === task.id} 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 dayjs from 'dayjs'; import dayjs from 'dayjs';
import { CaretDownFilled, CaretRightFilled } from '@ant-design/icons'; 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 { interface EnhancedKanbanTaskCardProps {
task: IProjectTask; task: IProjectTask;
sectionId: string;
isActive?: boolean; isActive?: boolean;
isDragOverlay?: boolean; isDragOverlay?: boolean;
isDropTarget?: 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(({ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo(({
task, task,
sectionId,
isActive = false, isActive = false,
isDragOverlay = false, isDragOverlay = false,
isDropTarget = false isDropTarget = false
}) => { }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation('kanban-board');
const themeMode = useAppSelector(state => state.themeReducer.mode); const themeMode = useAppSelector(state => state.themeReducer.mode);
const [showNewSubtaskCard, setShowNewSubtaskCard] = useState(false);
const [dueDate, setDueDate] = useState<Dayjs | null>( const [dueDate, setDueDate] = useState<Dayjs | null>(
task?.end_date ? dayjs(task?.end_date) : null task?.end_date ? dayjs(task?.end_date) : null
); );
const [isSubTaskShow, setIsSubTaskShow] = useState(false); const [isSubTaskShow, setIsSubTaskShow] = useState(false);
const projectId = useAppSelector(state => state.projectReducer.projectId);
const { const {
attributes, attributes,
listeners, listeners,
@@ -70,14 +90,8 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
// Don't handle click if we're dragging // Don't handle click if we're dragging
if (isDragging) return; if (isDragging) return;
dispatch(setSelectedTaskId(id));
// Add a small delay to ensure it's a click and not the start of a drag dispatch(setShowTaskDrawer(true));
const clickTimeout = setTimeout(() => {
dispatch(setSelectedTaskId(id));
dispatch(setShowTaskDrawer(true));
}, 50);
return () => clearTimeout(clickTimeout);
}, [dispatch, isDragging]); }, [dispatch, isDragging]);
const renderLabels = useMemo(() => { const renderLabels = useMemo(() => {
@@ -97,6 +111,32 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
); );
}, [task.labels, themeMode]); }, [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 ( return (
<div <div
ref={setNodeRef} ref={setNodeRef}
@@ -117,7 +157,10 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
</Flex> </Flex>
<Flex gap={4} align="center"> <Flex gap={4} align="center">
{/* Action Icons */} {/* Action Icons */}
<PrioritySection task={task} /> <div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: task.priority_color || '#d9d9d9' }}
/>
<Typography.Text <Typography.Text
style={{ fontWeight: 500 }} style={{ fontWeight: 500 }}
ellipsis={{ tooltip: task.name }} ellipsis={{ tooltip: task.name }}
@@ -126,42 +169,81 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
</Typography.Text> </Typography.Text>
</Flex> </Flex>
<Flex <Flex
align="center" align="center"
justify="space-between" justify="space-between"
style={{ style={{
marginBlock: 8, marginBlock: 8,
}} }}
> >
{task && <CustomAvatarGroup task={task} sectionId={task.status_id || ''} />} {task && <CustomAvatarGroup task={task} sectionId={sectionId} />}
<Flex gap={4} align="center"> <Flex gap={4} align="center">
<CustomDueDatePicker task={task} onDateChange={setDueDate} /> <CustomDueDatePicker task={task} onDateChange={setDueDate} />
{/* Subtask Section */} {/* Subtask Section */}
<Button <Button
// onClick={handleSubtaskButtonClick} onClick={handleSubtaskButtonClick}
size="small" size="small"
style={{
padding: 0,
}}
type="text"
>
<Tag
bordered={false}
style={{ style={{
padding: 0, display: 'flex',
alignItems: 'center',
margin: 0,
backgroundColor: themeWiseColor('white', '#1e1e1e', themeMode),
}} }}
type="text"
> >
<Tag <ForkOutlined rotate={90} />
bordered={false} <span>{task.sub_tasks_count}</span>
style={{ {isSubTaskShow ? <CaretDownFilled /> : <CaretRightFilled />}
display: 'flex', </Tag>
alignItems: 'center', </Button>
margin: 0, </Flex>
backgroundColor: themeWiseColor('white', '#1e1e1e', themeMode), </Flex>
}} <Flex vertical gap={8}>
> {isSubTaskShow && (
<ForkOutlined rotate={90} /> <Flex vertical>
<span>{task.sub_tasks_count}</span> <Divider style={{ marginBlock: 0 }} />
{isSubTaskShow ? <CaretDownFilled /> : <CaretRightFilled />} <List>
</Tag> {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> </Button>
</Flex> </Flex>
</Flex> )}
</Flex>
</div> </div>
</div> </div>
); );

View File

@@ -38,7 +38,7 @@ const VirtualizedTaskList: React.FC<VirtualizedTaskListProps> = ({
onTaskRender?.(task, index); onTaskRender?.(task, index);
return ( return (
<div style={style} className="virtualized-task-row"> <div className="virtualized-task-row">
<EnhancedKanbanTaskCard <EnhancedKanbanTaskCard
task={task} task={task}
isActive={task.id === activeTaskId} 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({ const enhancedKanbanSlice = createSlice({
name: 'enhancedKanbanReducer', name: 'enhancedKanbanReducer',
initialState, initialState,

View File

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