Merge pull request #211 from shancds/fix/enhanced-board-sub-task-section

Fix/enhanced board sub task section
This commit is contained in:
Chamika J
2025-07-01 12:11:18 +05:30
committed by GitHub
7 changed files with 445 additions and 50 deletions

View File

@@ -49,6 +49,7 @@ import EnhancedKanbanCreateSection from './EnhancedKanbanCreateSection';
import ImprovedTaskFilters from '../task-management/improved-task-filters';
import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
import { useFilterDataLoader } from '@/hooks/useFilterDataLoader';
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
// Import the TaskListFilters component
const TaskListFilters = React.lazy(() => import('@/pages/projects/projectView/taskList/task-list-filters/task-list-filters'));
@@ -75,6 +76,9 @@ const EnhancedKanbanBoard: React.FC<EnhancedKanbanBoardProps> = ({ projectId, cl
// Load filter data
useFilterDataLoader();
// Set up socket event handlers for real-time updates
useTaskSocketHandlers();
// Local state for drag overlay
const [activeTask, setActiveTask] = useState<any>(null);

View File

@@ -0,0 +1,173 @@
import { Flex, Input, InputRef } from 'antd';
import React, { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { updateEnhancedKanbanTaskProgress } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { themeWiseColor } from '@/utils/themeWiseColor';
import { useAppSelector } from '@/hooks/useAppSelector';
import { getCurrentGroup } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { useAuthService } from '@/hooks/useAuth';
import { ITaskCreateRequest } from '@/types/tasks/task-create-request.types';
import { useParams } from 'react-router-dom';
import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import logger from '@/utils/errorLogger';
type EnhancedKanbanCreateSubtaskCardProps = {
sectionId: string;
parentTaskId: string;
setShowNewSubtaskCard: (x: boolean) => void;
};
const EnhancedKanbanCreateSubtaskCard = ({
sectionId,
parentTaskId,
setShowNewSubtaskCard,
}: EnhancedKanbanCreateSubtaskCardProps) => {
const { socket, connected } = useSocket();
const dispatch = useAppDispatch();
const [creatingTask, setCreatingTask] = useState<boolean>(false);
const [newSubtaskName, setNewSubtaskName] = useState<string>('');
const [isEnterKeyPressed, setIsEnterKeyPressed] = useState<boolean>(false);
const cardRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<InputRef>(null);
const { t } = useTranslation('kanban-board');
const themeMode = useAppSelector(state => state.themeReducer.mode);
const { projectId } = useParams();
const currentSession = useAuthService().getCurrentSession();
const createRequestBody = (): ITaskCreateRequest | null => {
if (!projectId || !currentSession) return null;
const body: ITaskCreateRequest = {
project_id: projectId,
name: newSubtaskName,
reporter_id: currentSession.id,
team_id: currentSession.team_id,
};
const groupBy = getCurrentGroup();
if (groupBy === 'status') {
body.status_id = sectionId || undefined;
} else if (groupBy === 'priority') {
body.priority_id = sectionId || undefined;
} else if (groupBy === 'phase') {
body.phase_id = sectionId || undefined;
}
if (parentTaskId) {
body.parent_task_id = parentTaskId;
}
return body;
};
const handleAddSubtask = () => {
if (creatingTask || !projectId || !currentSession || newSubtaskName.trim() === '' || !connected)
return;
try {
setCreatingTask(true);
const body = createRequestBody();
if (!body) return;
socket?.emit(SocketEvents.QUICK_TASK.toString(), JSON.stringify(body));
socket?.once(SocketEvents.QUICK_TASK.toString(), (task: IProjectTask) => {
if (!task) return;
setCreatingTask(false);
setNewSubtaskName('');
setTimeout(() => {
inputRef.current?.focus();
}, 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,
}));
});
}
});
} catch (error) {
logger.error('Error adding task:', error);
setCreatingTask(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
setIsEnterKeyPressed(true);
handleAddSubtask();
}
};
const handleInputBlur = () => {
if (!isEnterKeyPressed && newSubtaskName.length > 0) {
handleAddSubtask();
}
setIsEnterKeyPressed(false);
};
const handleCancelNewCard = (e: React.FocusEvent<HTMLDivElement>) => {
if (cardRef.current && !cardRef.current.contains(e.relatedTarget)) {
setNewSubtaskName('');
setShowNewSubtaskCard(false);
}
};
return (
<Flex
ref={cardRef}
vertical
gap={4}
style={{
width: '100%',
padding: 2,
backgroundColor: themeMode === 'dark' ? '#292929' : '#fafafa',
borderRadius: 6,
cursor: 'pointer',
overflow: 'hidden',
}}
// className={`outline-1 ${themeWiseColor('outline-[#edeae9]', 'outline-[#6a696a]', themeMode)} hover:outline`}
onBlur={handleCancelNewCard}
>
<Input
ref={inputRef}
value={newSubtaskName}
onClick={e => e.stopPropagation()}
onChange={e => setNewSubtaskName(e.target.value)}
onKeyDown={e => {
e.stopPropagation();
if (e.key === 'Enter') {
setIsEnterKeyPressed(true);
handleAddSubtask();
}
}}
onKeyUp={e => e.stopPropagation()}
onKeyPress={e => e.stopPropagation()}
onBlur={handleInputBlur}
placeholder={t('kanbanBoard.addSubTaskPlaceholder')}
className={`enhanced-kanban-create-subtask-input ${themeMode === 'dark' ? 'dark' : ''}`}
disabled={creatingTask}
autoFocus
/>
</Flex>
);
};
export default EnhancedKanbanCreateSubtaskCard;

View File

@@ -20,7 +20,7 @@ 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 { fetchBoardSubTasks, toggleTaskExpansion } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { Divider } from 'antd';
import { List } from 'antd';
import { Skeleton } from 'antd';
@@ -28,6 +28,7 @@ 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';
import EnhancedKanbanCreateSubtaskCard from './EnhancedKanbanCreateSubtaskCard';
interface EnhancedKanbanTaskCardProps {
task: IProjectTask;
@@ -58,7 +59,7 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
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,
@@ -115,13 +116,17 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
const handleSubTaskExpand = useCallback(() => {
if (task && task.id && projectId) {
if (task.show_sub_tasks) {
// 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
setIsSubTaskShow(prev => !prev);
} else {
// If subtasks need to be fetched, show the section first with loading state
setIsSubTaskShow(true);
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]);
@@ -199,13 +204,13 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
>
<ForkOutlined rotate={90} />
<span>{task.sub_tasks_count}</span>
{isSubTaskShow ? <CaretDownFilled /> : <CaretRightFilled />}
{task.show_sub_tasks ? <CaretDownFilled /> : <CaretRightFilled />}
</Tag>
</Button>
</Flex>
</Flex>
<Flex vertical gap={8}>
{isSubTaskShow && (
{task.show_sub_tasks && (
<Flex vertical>
<Divider style={{ marginBlock: 0 }} />
<List>
@@ -215,13 +220,21 @@ const EnhancedKanbanTaskCard: React.FC<EnhancedKanbanTaskCardProps> = React.memo
</List.Item>
)}
{!task.sub_tasks_loading && task?.sub_tasks &&
task?.sub_tasks.map((subtask: any) => (
{!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 && (
<BoardCreateSubtaskCard
<EnhancedKanbanCreateSubtaskCard
sectionId={sectionId}
parentTaskId={task.id || ''}
setShowNewSubtaskCard={setShowNewSubtaskCard}

View File

@@ -14,6 +14,8 @@ import { ITaskViewModel } from '@/types/tasks/task.types';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { setStartDate, setTaskEndDate } from '@/features/task-drawer/task-drawer.slice';
import { updateEnhancedKanbanTaskStartDate, updateEnhancedKanbanTaskEndDate } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import useTabSearchParam from '@/hooks/useTabSearchParam';
interface TaskDrawerDueDateProps {
task: ITaskViewModel;
t: TFunction;
@@ -24,6 +26,7 @@ const TaskDrawerDueDate = ({ task, t, form }: TaskDrawerDueDateProps) => {
const { socket } = useSocket();
const [isShowStartDate, setIsShowStartDate] = useState(false);
const dispatch = useAppDispatch();
const { tab } = useTabSearchParam();
// Date handling
const startDayjs = task?.start_date ? dayjs(task.start_date) : null;
const dueDayjs = task?.end_date ? dayjs(task.end_date) : null;
@@ -63,6 +66,10 @@ const TaskDrawerDueDate = ({ task, t, form }: TaskDrawerDueDateProps) => {
(data: IProjectTask) => {
dispatch(setStartDate(data));
// Also update enhanced kanban if on board tab
if (tab === 'board') {
dispatch(updateEnhancedKanbanTaskStartDate({ task: data }));
}
}
);
} catch (error) {
@@ -88,6 +95,10 @@ const TaskDrawerDueDate = ({ task, t, form }: TaskDrawerDueDateProps) => {
(data: IProjectTask) => {
dispatch(setTaskEndDate(data));
// Also update enhanced kanban if on board tab
if (tab === 'board') {
dispatch(updateEnhancedKanbanTaskEndDate({ task: data }));
}
}
);
} catch (error) {

View File

@@ -92,6 +92,9 @@ const SubTaskTable = ({ subTasks, loadingSubTasks, refreshSubTasks, t }: SubTask
if (task.parent_task_id) {
refreshSubTasks();
dispatch(updateSubtask({ sectionId: '', subtask: task, mode: 'add' }));
// Note: Enhanced kanban updates are now handled by the global socket handler
// No need to dispatch here as it will be handled by useTaskSocketHandlers
}
});
} catch (error) {
@@ -109,6 +112,10 @@ const SubTaskTable = ({ subTasks, loadingSubTasks, refreshSubTasks, t }: SubTask
try {
await tasksApiService.deleteTask(taskId);
dispatch(updateSubtask({ sectionId: '', subtask: { id: taskId, parent_task_id: selectedTaskId || '' }, mode: 'delete' }));
// Note: Enhanced kanban updates are now handled by the global socket handler
// No need to dispatch here as it will be handled by useTaskSocketHandlers
refreshSubTasks();
} catch (error) {
logger.error('Error deleting subtask:', error);