Merge pull request #185 from shancds/refact/board-task-card-performance
Refact/board task card performance
This commit is contained in:
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
Reference in New Issue
Block a user