expand sub tasks
This commit is contained in:
@@ -6,47 +6,40 @@ import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||
import { IGroupBy } from '@/features/tasks/tasks.slice';
|
||||
|
||||
interface SortableKanbanGroupProps {
|
||||
group: ITaskListGroup;
|
||||
projectId: string;
|
||||
currentGrouping: IGroupBy;
|
||||
selectedTaskIds: string[];
|
||||
onAddTask?: (groupId: string) => void;
|
||||
onToggleCollapse?: (groupId: string) => void;
|
||||
onSelectTask?: (taskId: string, selected: boolean) => void;
|
||||
onToggleSubtasks?: (taskId: string) => void;
|
||||
activeTaskId?: string | null;
|
||||
group: ITaskListGroup;
|
||||
projectId: string;
|
||||
currentGrouping: IGroupBy;
|
||||
selectedTaskIds: string[];
|
||||
onAddTask?: (groupId: string) => void;
|
||||
onToggleCollapse?: (groupId: string) => void;
|
||||
onSelectTask?: (taskId: string, selected: boolean) => void;
|
||||
onToggleSubtasks?: (taskId: string) => void;
|
||||
activeTaskId?: string | null;
|
||||
}
|
||||
|
||||
const SortableKanbanGroup: React.FC<SortableKanbanGroupProps> = (props) => {
|
||||
const { group, activeTaskId } = props;
|
||||
const {
|
||||
setNodeRef,
|
||||
attributes,
|
||||
listeners,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id: group.id,
|
||||
data: { type: 'group', groupId: group.id },
|
||||
});
|
||||
const SortableKanbanGroup: React.FC<SortableKanbanGroupProps> = props => {
|
||||
const { group, activeTaskId } = props;
|
||||
const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
|
||||
id: group.id,
|
||||
data: { type: 'group', groupId: group.id },
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
zIndex: isDragging ? 10 : undefined,
|
||||
};
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
zIndex: isDragging ? 10 : undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style}>
|
||||
<KanbanGroup
|
||||
{...props}
|
||||
dragHandleProps={{ ...attributes, ...listeners }}
|
||||
activeTaskId={activeTaskId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div ref={setNodeRef} style={style}>
|
||||
<KanbanGroup
|
||||
{...props}
|
||||
dragHandleProps={{ ...attributes, ...listeners }}
|
||||
activeTaskId={activeTaskId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SortableKanbanGroup;
|
||||
export default SortableKanbanGroup;
|
||||
|
||||
@@ -10,130 +10,122 @@ import KanbanTaskCard from './kanbanTaskCard';
|
||||
const { Text } = Typography;
|
||||
|
||||
interface TaskGroupProps {
|
||||
group: ITaskListGroup;
|
||||
projectId: string;
|
||||
currentGrouping: IGroupBy;
|
||||
selectedTaskIds: string[];
|
||||
onAddTask?: (groupId: string) => void;
|
||||
onToggleCollapse?: (groupId: string) => void;
|
||||
onSelectTask?: (taskId: string, selected: boolean) => void;
|
||||
onToggleSubtasks?: (taskId: string) => void;
|
||||
dragHandleProps?: any;
|
||||
activeTaskId?: string | null;
|
||||
group: ITaskListGroup;
|
||||
projectId: string;
|
||||
currentGrouping: IGroupBy;
|
||||
selectedTaskIds: string[];
|
||||
onAddTask?: (groupId: string) => void;
|
||||
onToggleCollapse?: (groupId: string) => void;
|
||||
onSelectTask?: (taskId: string, selected: boolean) => void;
|
||||
onToggleSubtasks?: (taskId: string) => void;
|
||||
dragHandleProps?: any;
|
||||
activeTaskId?: string | null;
|
||||
}
|
||||
|
||||
const KanbanGroup: React.FC<TaskGroupProps> = ({
|
||||
group,
|
||||
projectId,
|
||||
currentGrouping,
|
||||
selectedTaskIds,
|
||||
onAddTask,
|
||||
onToggleCollapse,
|
||||
onSelectTask,
|
||||
onToggleSubtasks,
|
||||
dragHandleProps,
|
||||
activeTaskId,
|
||||
group,
|
||||
projectId,
|
||||
currentGrouping,
|
||||
selectedTaskIds,
|
||||
onAddTask,
|
||||
onToggleCollapse,
|
||||
onSelectTask,
|
||||
onToggleSubtasks,
|
||||
dragHandleProps,
|
||||
activeTaskId,
|
||||
}) => {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const { setNodeRef, isOver } = useDroppable({
|
||||
id: group.id,
|
||||
data: {
|
||||
type: 'group',
|
||||
groupId: group.id,
|
||||
},
|
||||
});
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const { setNodeRef, isOver } = useDroppable({
|
||||
id: group.id,
|
||||
data: {
|
||||
type: 'group',
|
||||
groupId: group.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Get task IDs for sortable context
|
||||
const taskIds = group.tasks.map(task => task.id!);
|
||||
// Get task IDs for sortable context
|
||||
const taskIds = group.tasks.map(task => task.id!);
|
||||
|
||||
// Get group color based on grouping type
|
||||
const getGroupColor = () => {
|
||||
if (group.color_code) return group.color_code;
|
||||
switch (currentGrouping) {
|
||||
case 'status':
|
||||
return group.id === 'todo' ? '#faad14' : group.id === 'doing' ? '#1890ff' : '#52c41a';
|
||||
case 'priority':
|
||||
return group.id === 'critical'
|
||||
? '#ff4d4f'
|
||||
: group.id === 'high'
|
||||
? '#fa8c16'
|
||||
: group.id === 'medium'
|
||||
? '#faad14'
|
||||
: '#52c41a';
|
||||
case 'phase':
|
||||
return '#722ed1';
|
||||
default:
|
||||
return '#d9d9d9';
|
||||
}
|
||||
};
|
||||
// Get group color based on grouping type
|
||||
const getGroupColor = () => {
|
||||
if (group.color_code) return group.color_code;
|
||||
switch (currentGrouping) {
|
||||
case 'status':
|
||||
return group.id === 'todo' ? '#faad14' : group.id === 'doing' ? '#1890ff' : '#52c41a';
|
||||
case 'priority':
|
||||
return group.id === 'critical'
|
||||
? '#ff4d4f'
|
||||
: group.id === 'high'
|
||||
? '#fa8c16'
|
||||
: group.id === 'medium'
|
||||
? '#faad14'
|
||||
: '#52c41a';
|
||||
case 'phase':
|
||||
return '#722ed1';
|
||||
default:
|
||||
return '#d9d9d9';
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddTask = () => {
|
||||
onAddTask?.(group.id);
|
||||
};
|
||||
const handleAddTask = () => {
|
||||
onAddTask?.(group.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={`kanban-group-column${isOver ? ' drag-over' : ''}`}
|
||||
>
|
||||
{/* Group Header */}
|
||||
<div className="kanban-group-header" style={{ backgroundColor: getGroupColor() }}>
|
||||
{/* Drag handle for column */}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<MenuOutlined />}
|
||||
className="kanban-group-drag-handle"
|
||||
style={{ marginRight: 8, cursor: 'grab', opacity: 0.7 }}
|
||||
{...(dragHandleProps || {})}
|
||||
return (
|
||||
<div ref={setNodeRef} className={`kanban-group-column${isOver ? ' drag-over' : ''}`}>
|
||||
{/* Group Header */}
|
||||
<div className="kanban-group-header" style={{ backgroundColor: getGroupColor() }}>
|
||||
{/* Drag handle for column */}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<MenuOutlined />}
|
||||
className="kanban-group-drag-handle"
|
||||
style={{ marginRight: 8, cursor: 'grab', opacity: 0.7 }}
|
||||
{...(dragHandleProps || {})}
|
||||
/>
|
||||
<Text strong className="kanban-group-header-text">
|
||||
{group.name} <span className="kanban-group-count">({group.tasks.length})</span>
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Tasks as Cards */}
|
||||
<SortableContext items={taskIds} strategy={verticalListSortingStrategy}>
|
||||
<div className="kanban-group-tasks">
|
||||
{group.tasks.length === 0 ? (
|
||||
<div className="kanban-group-empty">
|
||||
<Text type="secondary">No tasks in this group</Text>
|
||||
</div>
|
||||
) : (
|
||||
group.tasks.map((task, index) =>
|
||||
task.id === activeTaskId ? (
|
||||
<div key={task.id} className="kanban-task-card kanban-task-card-placeholder" />
|
||||
) : (
|
||||
<KanbanTaskCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
groupId={group.id}
|
||||
currentGrouping={currentGrouping}
|
||||
isSelected={selectedTaskIds.includes(task.id!)}
|
||||
index={index}
|
||||
onSelect={onSelectTask}
|
||||
onToggleSubtasks={onToggleSubtasks}
|
||||
/>
|
||||
<Text strong className="kanban-group-header-text">
|
||||
{group.name} <span className="kanban-group-count">({group.tasks.length})</span>
|
||||
</Text>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</SortableContext>
|
||||
|
||||
{/* Tasks as Cards */}
|
||||
<SortableContext items={taskIds} strategy={verticalListSortingStrategy}>
|
||||
<div className="kanban-group-tasks">
|
||||
{group.tasks.length === 0 ? (
|
||||
<div className="kanban-group-empty">
|
||||
<Text type="secondary">No tasks in this group</Text>
|
||||
</div>
|
||||
) : (
|
||||
group.tasks.map((task, index) => (
|
||||
task.id === activeTaskId ? (
|
||||
<div key={task.id} className="kanban-task-card kanban-task-card-placeholder" />
|
||||
) : (
|
||||
<KanbanTaskCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
groupId={group.id}
|
||||
currentGrouping={currentGrouping}
|
||||
isSelected={selectedTaskIds.includes(task.id!)}
|
||||
index={index}
|
||||
onSelect={onSelectTask}
|
||||
onToggleSubtasks={onToggleSubtasks}
|
||||
/>
|
||||
)
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</SortableContext>
|
||||
{/* Add Task Button */}
|
||||
<div className="kanban-group-add-task">
|
||||
<Button type="dashed" icon={<PlusOutlined />} block onClick={handleAddTask}>
|
||||
Add Task
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Add Task Button */}
|
||||
<div className="kanban-group-add-task">
|
||||
<Button
|
||||
type="dashed"
|
||||
icon={<PlusOutlined />}
|
||||
block
|
||||
onClick={handleAddTask}
|
||||
>
|
||||
Add Task
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
<style>{`
|
||||
.kanban-group-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -221,8 +213,8 @@ const KanbanGroup: React.FC<TaskGroupProps> = ({
|
||||
border: 2px dashed var(--task-drag-over-border, #40a9ff);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KanbanGroup;
|
||||
|
||||
@@ -36,14 +36,7 @@ const KanbanTaskCard: React.FC<TaskRowProps> = ({
|
||||
onSelect,
|
||||
onToggleSubtasks,
|
||||
}) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: task.id!,
|
||||
data: {
|
||||
type: 'task',
|
||||
@@ -93,7 +86,10 @@ const KanbanTaskCard: React.FC<TaskRowProps> = ({
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
/>
|
||||
<Text strong className={`kanban-task-title${task.complete_ratio === 100 ? ' kanban-task-completed' : ''}`}>
|
||||
<Text
|
||||
strong
|
||||
className={`kanban-task-title${task.complete_ratio === 100 ? ' kanban-task-completed' : ''}`}
|
||||
>
|
||||
{task.name}
|
||||
</Text>
|
||||
{task.sub_tasks_count && task.sub_tasks_count > 0 && (
|
||||
@@ -112,15 +108,23 @@ const KanbanTaskCard: React.FC<TaskRowProps> = ({
|
||||
{/* Task Key and Status */}
|
||||
<div className="kanban-task-row">
|
||||
{task.task_key && (
|
||||
<Text code className="kanban-task-key">{task.task_key}</Text>
|
||||
<Text code className="kanban-task-key">
|
||||
{task.task_key}
|
||||
</Text>
|
||||
)}
|
||||
{task.status_name && (
|
||||
<Tag className="kanban-task-status" style={{ backgroundColor: task.status_color, color: 'white', marginLeft: 8 }}>
|
||||
<Tag
|
||||
className="kanban-task-status"
|
||||
style={{ backgroundColor: task.status_color, color: 'white', marginLeft: 8 }}
|
||||
>
|
||||
{task.status_name}
|
||||
</Tag>
|
||||
)}
|
||||
{task.priority_name && (
|
||||
<Tag className="kanban-task-priority" style={{ backgroundColor: task.priority_color, color: 'white', marginLeft: 8 }}>
|
||||
<Tag
|
||||
className="kanban-task-priority"
|
||||
style={{ backgroundColor: task.priority_color, color: 'white', marginLeft: 8 }}
|
||||
>
|
||||
{task.priority_name}
|
||||
</Tag>
|
||||
)}
|
||||
@@ -139,7 +143,11 @@ const KanbanTaskCard: React.FC<TaskRowProps> = ({
|
||||
/>
|
||||
)}
|
||||
{dueDate && (
|
||||
<Text type={dueDate.color as any} className="kanban-task-due-date" style={{ marginLeft: 12 }}>
|
||||
<Text
|
||||
type={dueDate.color as any}
|
||||
className="kanban-task-due-date"
|
||||
style={{ marginLeft: 12 }}
|
||||
>
|
||||
<ClockCircleOutlined style={{ marginRight: 4 }} />
|
||||
{dueDate.text}
|
||||
</Text>
|
||||
@@ -149,7 +157,7 @@ const KanbanTaskCard: React.FC<TaskRowProps> = ({
|
||||
<div className="kanban-task-row">
|
||||
{task.assignees && task.assignees.length > 0 && (
|
||||
<Avatar.Group size="small" maxCount={3}>
|
||||
{task.assignees.map((assignee) => (
|
||||
{task.assignees.map(assignee => (
|
||||
<Tooltip key={assignee.id} title={assignee.name}>
|
||||
<Avatar size="small">{assignee.name?.charAt(0)?.toUpperCase()}</Avatar>
|
||||
</Tooltip>
|
||||
@@ -158,11 +166,16 @@ const KanbanTaskCard: React.FC<TaskRowProps> = ({
|
||||
)}
|
||||
{task.labels && task.labels.length > 0 && (
|
||||
<div className="kanban-task-labels">
|
||||
{task.labels.slice(0, 2).map((label) => (
|
||||
{task.labels.slice(0, 2).map(label => (
|
||||
<Tag
|
||||
key={label.id}
|
||||
className="kanban-task-label"
|
||||
style={{ backgroundColor: label.color_code, border: 'none', color: 'white', marginLeft: 4 }}
|
||||
style={{
|
||||
backgroundColor: label.color_code,
|
||||
border: 'none',
|
||||
color: 'white',
|
||||
marginLeft: 4,
|
||||
}}
|
||||
>
|
||||
{label.name}
|
||||
</Tag>
|
||||
@@ -198,7 +211,7 @@ const KanbanTaskCard: React.FC<TaskRowProps> = ({
|
||||
{/* Subtasks */}
|
||||
{task.show_sub_tasks && task.sub_tasks && task.sub_tasks.length > 0 && (
|
||||
<div className="kanban-task-subtasks">
|
||||
{task.sub_tasks.map((subtask) => (
|
||||
{task.sub_tasks.map(subtask => (
|
||||
<KanbanTaskCard
|
||||
key={subtask.id}
|
||||
task={subtask}
|
||||
@@ -398,4 +411,4 @@ const KanbanTaskCard: React.FC<TaskRowProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default KanbanTaskCard;
|
||||
export default KanbanTaskCard;
|
||||
|
||||
@@ -1,30 +1,25 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import {
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
DragStartEvent,
|
||||
DragEndEvent,
|
||||
DragOverEvent,
|
||||
closestCorners,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
DragStartEvent,
|
||||
DragEndEvent,
|
||||
DragOverEvent,
|
||||
closestCorners,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
horizontalListSortingStrategy,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
horizontalListSortingStrategy,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { Card, Spin, Empty, Flex } from 'antd';
|
||||
import { RootState } from '@/app/store';
|
||||
import {
|
||||
IGroupBy,
|
||||
setGroup,
|
||||
fetchTaskGroups,
|
||||
reorderTasks,
|
||||
} from '@/features/tasks/tasks.slice';
|
||||
import { IGroupBy, setGroup, fetchTaskGroups, reorderTasks } from '@/features/tasks/tasks.slice';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import { AppDispatch } from '@/app/store';
|
||||
import BoardSectionCard from '@/pages/projects/projectView/board/board-section/board-section-card/board-section-card';
|
||||
@@ -38,269 +33,261 @@ import KanbanGroup from './kanbanGroup';
|
||||
import KanbanTaskCard from './kanbanTaskCard';
|
||||
import SortableKanbanGroup from './SortableKanbanGroup';
|
||||
|
||||
|
||||
// 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 TaskListBoardProps {
|
||||
projectId: string;
|
||||
className?: string;
|
||||
projectId: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface DragState {
|
||||
activeTask: IProjectTask | null;
|
||||
activeGroupId: string | null;
|
||||
activeTask: IProjectTask | null;
|
||||
activeGroupId: string | null;
|
||||
}
|
||||
|
||||
const KanbanTaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = '' }) => {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const [dragState, setDragState] = useState<DragState>({
|
||||
activeTask: null,
|
||||
activeGroupId: null,
|
||||
});
|
||||
// New state for active/over ids
|
||||
const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
|
||||
const [overId, setOverId] = useState<string | null>(null);
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const [dragState, setDragState] = useState<DragState>({
|
||||
activeTask: null,
|
||||
activeGroupId: null,
|
||||
});
|
||||
// New state for active/over ids
|
||||
const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
|
||||
const [overId, setOverId] = useState<string | null>(null);
|
||||
|
||||
// Redux selectors
|
||||
// Redux selectors
|
||||
|
||||
const { taskGroups, groupBy, loadingGroups, error, search, archived } = useSelector((state: RootState) => state.boardReducer);
|
||||
const { taskGroups, groupBy, loadingGroups, error, search, archived } = useSelector(
|
||||
(state: RootState) => state.boardReducer
|
||||
);
|
||||
|
||||
// Selection state
|
||||
const [selectedTaskIds, setSelectedTaskIds] = useState<string[]>([]);
|
||||
// Selection state
|
||||
const [selectedTaskIds, setSelectedTaskIds] = useState<string[]>([]);
|
||||
|
||||
// Drag and Drop sensors
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
},
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
);
|
||||
const isOwnerorAdmin = useAuthService().isOwnerOrAdmin();
|
||||
const isProjectManager = useIsProjectManager();
|
||||
// Drag and Drop sensors
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
},
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
);
|
||||
const isOwnerorAdmin = useAuthService().isOwnerOrAdmin();
|
||||
const isProjectManager = useIsProjectManager();
|
||||
|
||||
// Fetch task groups when component mounts or dependencies change
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
dispatch(fetchTaskGroups(projectId));
|
||||
}
|
||||
}, [dispatch, projectId, groupBy, search, archived]);
|
||||
|
||||
// Memoized calculations
|
||||
const allTaskIds = useMemo(() => {
|
||||
return taskGroups.flatMap(group => group.tasks.map(task => task.id!));
|
||||
}, [taskGroups]);
|
||||
|
||||
const totalTasksCount = useMemo(() => {
|
||||
return taskGroups.reduce((sum, group) => sum + group.tasks.length, 0);
|
||||
}, [taskGroups]);
|
||||
|
||||
const hasSelection = selectedTaskIds.length > 0;
|
||||
|
||||
// // Handlers
|
||||
// const handleGroupingChange = (newGroupBy: IGroupBy) => {
|
||||
// dispatch(setGroup(newGroupBy));
|
||||
// };
|
||||
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
const { active } = event;
|
||||
const taskId = active.id as string;
|
||||
setActiveTaskId(taskId);
|
||||
setOverId(null);
|
||||
// Find the task and its group
|
||||
let activeTask: IProjectTask | null = null;
|
||||
let activeGroupId: string | null = null;
|
||||
for (const group of taskGroups) {
|
||||
const task = group.tasks.find(t => t.id === taskId);
|
||||
if (task) {
|
||||
activeTask = task;
|
||||
activeGroupId = group.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
setDragState({
|
||||
activeTask,
|
||||
activeGroupId,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDragOver = (event: DragOverEvent) => {
|
||||
setOverId(event.over?.id as string || null);
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
setActiveTaskId(null);
|
||||
setOverId(null);
|
||||
setDragState({
|
||||
activeTask: null,
|
||||
activeGroupId: null,
|
||||
});
|
||||
if (!over || !dragState.activeTask || !dragState.activeGroupId) {
|
||||
return;
|
||||
}
|
||||
const activeTaskId = active.id as string;
|
||||
const overIdVal = over.id as string;
|
||||
// Find the group and index for drop
|
||||
let targetGroupId = overIdVal;
|
||||
let targetIndex = -1;
|
||||
let isOverTask = false;
|
||||
// Check if over is a group or a task
|
||||
const overGroup = taskGroups.find(g => g.id === overIdVal);
|
||||
if (!overGroup) {
|
||||
// Dropping on a task, find which group it belongs to
|
||||
for (const group of taskGroups) {
|
||||
const taskIndex = group.tasks.findIndex(t => t.id === overIdVal);
|
||||
if (taskIndex !== -1) {
|
||||
targetGroupId = group.id;
|
||||
targetIndex = taskIndex;
|
||||
isOverTask = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const sourceGroup = taskGroups.find(g => g.id === dragState.activeGroupId);
|
||||
const targetGroup = taskGroups.find(g => g.id === targetGroupId);
|
||||
if (!sourceGroup || !targetGroup) return;
|
||||
const sourceIndex = sourceGroup.tasks.findIndex(t => t.id === activeTaskId);
|
||||
if (sourceIndex === -1) return;
|
||||
// Calculate new positions
|
||||
let finalTargetIndex = targetIndex;
|
||||
if (!isOverTask || finalTargetIndex === -1) {
|
||||
finalTargetIndex = targetGroup.tasks.length;
|
||||
}
|
||||
// If moving within the same group and after itself, adjust index
|
||||
if (sourceGroup.id === targetGroup.id && sourceIndex < finalTargetIndex) {
|
||||
finalTargetIndex--;
|
||||
}
|
||||
// Create updated task arrays
|
||||
const updatedSourceTasks = [...sourceGroup.tasks];
|
||||
const [movedTask] = updatedSourceTasks.splice(sourceIndex, 1);
|
||||
let updatedTargetTasks: IProjectTask[];
|
||||
if (sourceGroup.id === targetGroup.id) {
|
||||
updatedTargetTasks = updatedSourceTasks;
|
||||
updatedTargetTasks.splice(finalTargetIndex, 0, movedTask);
|
||||
} else {
|
||||
updatedTargetTasks = [...targetGroup.tasks];
|
||||
updatedTargetTasks.splice(finalTargetIndex, 0, movedTask);
|
||||
}
|
||||
// Dispatch the reorder action
|
||||
dispatch(reorderTasks({
|
||||
activeGroupId: sourceGroup.id,
|
||||
overGroupId: targetGroup.id,
|
||||
fromIndex: sourceIndex,
|
||||
toIndex: finalTargetIndex,
|
||||
task: movedTask,
|
||||
updatedSourceTasks,
|
||||
updatedTargetTasks,
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleSelectTask = (taskId: string, selected: boolean) => {
|
||||
setSelectedTaskIds(prev => {
|
||||
if (selected) {
|
||||
return [...prev, taskId];
|
||||
} else {
|
||||
return prev.filter(id => id !== taskId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleSubtasks = (taskId: string) => {
|
||||
// Implementation for toggling subtasks
|
||||
console.log('Toggle subtasks for task:', taskId);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card className={className}>
|
||||
<Empty
|
||||
description={`Error loading tasks: ${error}`}
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
// Fetch task groups when component mounts or dependencies change
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
dispatch(fetchTaskGroups(projectId));
|
||||
}
|
||||
}, [dispatch, projectId, groupBy, search, archived]);
|
||||
|
||||
// Memoized calculations
|
||||
const allTaskIds = useMemo(() => {
|
||||
return taskGroups.flatMap(group => group.tasks.map(task => task.id!));
|
||||
}, [taskGroups]);
|
||||
|
||||
const totalTasksCount = useMemo(() => {
|
||||
return taskGroups.reduce((sum, group) => sum + group.tasks.length, 0);
|
||||
}, [taskGroups]);
|
||||
|
||||
const hasSelection = selectedTaskIds.length > 0;
|
||||
|
||||
// // Handlers
|
||||
// const handleGroupingChange = (newGroupBy: IGroupBy) => {
|
||||
// dispatch(setGroup(newGroupBy));
|
||||
// };
|
||||
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
const { active } = event;
|
||||
const taskId = active.id as string;
|
||||
setActiveTaskId(taskId);
|
||||
setOverId(null);
|
||||
// Find the task and its group
|
||||
let activeTask: IProjectTask | null = null;
|
||||
let activeGroupId: string | null = null;
|
||||
for (const group of taskGroups) {
|
||||
const task = group.tasks.find(t => t.id === taskId);
|
||||
if (task) {
|
||||
activeTask = task;
|
||||
activeGroupId = group.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
setDragState({
|
||||
activeTask,
|
||||
activeGroupId,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDragOver = (event: DragOverEvent) => {
|
||||
setOverId((event.over?.id as string) || null);
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
setActiveTaskId(null);
|
||||
setOverId(null);
|
||||
setDragState({
|
||||
activeTask: null,
|
||||
activeGroupId: null,
|
||||
});
|
||||
if (!over || !dragState.activeTask || !dragState.activeGroupId) {
|
||||
return;
|
||||
}
|
||||
const activeTaskId = active.id as string;
|
||||
const overIdVal = over.id as string;
|
||||
// Find the group and index for drop
|
||||
let targetGroupId = overIdVal;
|
||||
let targetIndex = -1;
|
||||
let isOverTask = false;
|
||||
// Check if over is a group or a task
|
||||
const overGroup = taskGroups.find(g => g.id === overIdVal);
|
||||
if (!overGroup) {
|
||||
// Dropping on a task, find which group it belongs to
|
||||
for (const group of taskGroups) {
|
||||
const taskIndex = group.tasks.findIndex(t => t.id === overIdVal);
|
||||
if (taskIndex !== -1) {
|
||||
targetGroupId = group.id;
|
||||
targetIndex = taskIndex;
|
||||
isOverTask = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const sourceGroup = taskGroups.find(g => g.id === dragState.activeGroupId);
|
||||
const targetGroup = taskGroups.find(g => g.id === targetGroupId);
|
||||
if (!sourceGroup || !targetGroup) return;
|
||||
const sourceIndex = sourceGroup.tasks.findIndex(t => t.id === activeTaskId);
|
||||
if (sourceIndex === -1) return;
|
||||
// Calculate new positions
|
||||
let finalTargetIndex = targetIndex;
|
||||
if (!isOverTask || finalTargetIndex === -1) {
|
||||
finalTargetIndex = targetGroup.tasks.length;
|
||||
}
|
||||
// If moving within the same group and after itself, adjust index
|
||||
if (sourceGroup.id === targetGroup.id && sourceIndex < finalTargetIndex) {
|
||||
finalTargetIndex--;
|
||||
}
|
||||
// Create updated task arrays
|
||||
const updatedSourceTasks = [...sourceGroup.tasks];
|
||||
const [movedTask] = updatedSourceTasks.splice(sourceIndex, 1);
|
||||
let updatedTargetTasks: IProjectTask[];
|
||||
if (sourceGroup.id === targetGroup.id) {
|
||||
updatedTargetTasks = updatedSourceTasks;
|
||||
updatedTargetTasks.splice(finalTargetIndex, 0, movedTask);
|
||||
} else {
|
||||
updatedTargetTasks = [...targetGroup.tasks];
|
||||
updatedTargetTasks.splice(finalTargetIndex, 0, movedTask);
|
||||
}
|
||||
// Dispatch the reorder action
|
||||
dispatch(
|
||||
reorderTasks({
|
||||
activeGroupId: sourceGroup.id,
|
||||
overGroupId: targetGroup.id,
|
||||
fromIndex: sourceIndex,
|
||||
toIndex: finalTargetIndex,
|
||||
task: movedTask,
|
||||
updatedSourceTasks,
|
||||
updatedTargetTasks,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleSelectTask = (taskId: string, selected: boolean) => {
|
||||
setSelectedTaskIds(prev => {
|
||||
if (selected) {
|
||||
return [...prev, taskId];
|
||||
} else {
|
||||
return prev.filter(id => id !== taskId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleSubtasks = (taskId: string) => {
|
||||
// Implementation for toggling subtasks
|
||||
console.log('Toggle subtasks for task:', taskId);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={`task-list-board ${className}`}>
|
||||
{/* Task Filters */}
|
||||
<Card
|
||||
size="small"
|
||||
className="mb-4"
|
||||
styles={{ body: { padding: '12px 16px' } }}
|
||||
>
|
||||
<React.Suspense fallback={<div>Loading filters...</div>}>
|
||||
<TaskListFilters position="board" />
|
||||
</React.Suspense>
|
||||
</Card>
|
||||
<Card className={className}>
|
||||
<Empty description={`Error loading tasks: ${error}`} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`task-list-board ${className}`}>
|
||||
{/* Task Filters */}
|
||||
<Card size="small" className="mb-4" styles={{ body: { padding: '12px 16px' } }}>
|
||||
<React.Suspense fallback={<div>Loading filters...</div>}>
|
||||
<TaskListFilters position="board" />
|
||||
</React.Suspense>
|
||||
</Card>
|
||||
|
||||
{/* Task Groups Container */}
|
||||
<div className="task-groups-outer-container">
|
||||
{loadingGroups ? (
|
||||
<Card>
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
</Card>
|
||||
) : taskGroups.length === 0 ? (
|
||||
<Card>
|
||||
<Empty
|
||||
description="No tasks found"
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
/>
|
||||
</Card>
|
||||
) : (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCorners}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={taskGroups.map(g => g.id)}
|
||||
strategy={horizontalListSortingStrategy}
|
||||
>
|
||||
<div className="task-groups-container">
|
||||
{taskGroups.map((group) => (
|
||||
<SortableKanbanGroup
|
||||
key={group.id}
|
||||
group={group}
|
||||
projectId={projectId}
|
||||
currentGrouping={groupBy}
|
||||
selectedTaskIds={selectedTaskIds}
|
||||
onSelectTask={handleSelectTask}
|
||||
onToggleSubtasks={handleToggleSubtasks}
|
||||
activeTaskId={activeTaskId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
<DragOverlay>
|
||||
{dragState.activeTask ? (
|
||||
<KanbanTaskCard
|
||||
task={dragState.activeTask}
|
||||
projectId={projectId}
|
||||
groupId={dragState.activeGroupId!}
|
||||
currentGrouping={groupBy}
|
||||
isSelected={false}
|
||||
isDragOverlay
|
||||
/>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
)}
|
||||
{/* Task Groups Container */}
|
||||
<div className="task-groups-outer-container">
|
||||
{loadingGroups ? (
|
||||
<Card>
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
</Card>
|
||||
) : taskGroups.length === 0 ? (
|
||||
<Card>
|
||||
<Empty description="No tasks found" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</Card>
|
||||
) : (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCorners}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={taskGroups.map(g => g.id)}
|
||||
strategy={horizontalListSortingStrategy}
|
||||
>
|
||||
<div className="task-groups-container">
|
||||
{taskGroups.map(group => (
|
||||
<SortableKanbanGroup
|
||||
key={group.id}
|
||||
group={group}
|
||||
projectId={projectId}
|
||||
currentGrouping={groupBy}
|
||||
selectedTaskIds={selectedTaskIds}
|
||||
onSelectTask={handleSelectTask}
|
||||
onToggleSubtasks={handleToggleSubtasks}
|
||||
activeTaskId={activeTaskId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
<DragOverlay>
|
||||
{dragState.activeTask ? (
|
||||
<KanbanTaskCard
|
||||
task={dragState.activeTask}
|
||||
projectId={projectId}
|
||||
groupId={dragState.activeGroupId!}
|
||||
currentGrouping={groupBy}
|
||||
isSelected={false}
|
||||
isDragOverlay
|
||||
/>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
<style>{`
|
||||
.task-groups-outer-container {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
@@ -405,8 +392,8 @@ const KanbanTaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, classNam
|
||||
--task-drag-over-border: #40a9ff;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KanbanTaskListBoard;
|
||||
export default KanbanTaskListBoard;
|
||||
|
||||
Reference in New Issue
Block a user