Merge pull request #229 from shancds/test/row-kanban-board-v1.1.1
Test/row kanban board v1.1.1
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { RootState } from '@/app/store';
|
||||
import '../EnhancedKanbanBoard.css';
|
||||
import '../EnhancedKanbanGroup.css';
|
||||
import '../EnhancedKanbanTaskCard.css';
|
||||
import ImprovedTaskFilters from '../../task-management/improved-task-filters';
|
||||
import Card from 'antd/es/card';
|
||||
import Spin from 'antd/es/spin';
|
||||
import Empty from 'antd/es/empty';
|
||||
import { reorderGroups, reorderEnhancedKanbanGroups, reorderTasks, reorderEnhancedKanbanTasks, fetchEnhancedKanbanLabels, fetchEnhancedKanbanGroups, fetchEnhancedKanbanTaskAssignees } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||
import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import KanbanGroup from './KanbanGroup';
|
||||
import EnhancedKanbanCreateSection from '../EnhancedKanbanCreateSection';
|
||||
import { useSocket } from '@/socket/socketContext';
|
||||
import { SocketEvents } from '@/shared/socket-events';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import { statusApiService } from '@/api/taskAttributes/status/status.api.service';
|
||||
import alertService from '@/services/alerts/alertService';
|
||||
import logger from '@/utils/errorLogger';
|
||||
import Skeleton from 'antd/es/skeleton/Skeleton';
|
||||
|
||||
const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ projectId }) => {
|
||||
const dispatch = useDispatch();
|
||||
const authService = useAuthService();
|
||||
const { socket } = useSocket();
|
||||
const project = useAppSelector((state: RootState) => state.projectReducer.project);
|
||||
const groupBy = useSelector((state: RootState) => state.enhancedKanbanReducer.groupBy);
|
||||
const teamId = authService.getCurrentSession()?.team_id;
|
||||
const {
|
||||
taskGroups,
|
||||
loadingGroups,
|
||||
error,
|
||||
} = useSelector((state: RootState) => state.enhancedKanbanReducer);
|
||||
const [draggedGroupId, setDraggedGroupId] = useState<string | null>(null);
|
||||
const [draggedTaskId, setDraggedTaskId] = useState<string | null>(null);
|
||||
const [draggedTaskGroupId, setDraggedTaskGroupId] = useState<string | null>(null);
|
||||
const [hoveredGroupId, setHoveredGroupId] = useState<string | null>(null);
|
||||
const [hoveredTaskIdx, setHoveredTaskIdx] = useState<number | null>(null);
|
||||
const [dragType, setDragType] = useState<'group' | 'task' | null>(null);
|
||||
const { statusCategories, status: existingStatuses } = useAppSelector((state) => state.taskStatusReducer);
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
dispatch(fetchEnhancedKanbanGroups(projectId) as any);
|
||||
// Load filter data for enhanced kanban
|
||||
dispatch(fetchEnhancedKanbanTaskAssignees(projectId) as any);
|
||||
dispatch(fetchEnhancedKanbanLabels(projectId) as any);
|
||||
}
|
||||
|
||||
if (!statusCategories.length) {
|
||||
dispatch(fetchStatusesCategories() as any);
|
||||
}
|
||||
}, [dispatch, projectId]);
|
||||
// Reset drag state if taskGroups changes (e.g., real-time update)
|
||||
useEffect(() => {
|
||||
setDraggedGroupId(null);
|
||||
setDraggedTaskId(null);
|
||||
setDraggedTaskGroupId(null);
|
||||
setHoveredGroupId(null);
|
||||
setHoveredTaskIdx(null);
|
||||
setDragType(null);
|
||||
}, [taskGroups]);
|
||||
|
||||
// Group drag handlers
|
||||
const handleGroupDragStart = (e: React.DragEvent, groupId: string) => {
|
||||
setDraggedGroupId(groupId);
|
||||
setDragType('group');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
const handleGroupDragOver = (e: React.DragEvent) => {
|
||||
if (dragType !== 'group') return;
|
||||
e.preventDefault();
|
||||
};
|
||||
const handleGroupDrop = async (e: React.DragEvent, targetGroupId: string) => {
|
||||
if (dragType !== 'group') return;
|
||||
e.preventDefault();
|
||||
if (!draggedGroupId || draggedGroupId === targetGroupId) return;
|
||||
// Calculate new order and dispatch
|
||||
const fromIdx = taskGroups.findIndex(g => g.id === draggedGroupId);
|
||||
const toIdx = taskGroups.findIndex(g => g.id === targetGroupId);
|
||||
if (fromIdx === -1 || toIdx === -1) return;
|
||||
const reorderedGroups = [...taskGroups];
|
||||
const [moved] = reorderedGroups.splice(fromIdx, 1);
|
||||
reorderedGroups.splice(toIdx, 0, moved);
|
||||
dispatch(reorderGroups({ fromIndex: fromIdx, toIndex: toIdx, reorderedGroups }));
|
||||
dispatch(reorderEnhancedKanbanGroups({ fromIndex: fromIdx, toIndex: toIdx, reorderedGroups }) as any);
|
||||
|
||||
// API call for group order
|
||||
try {
|
||||
const columnOrder = reorderedGroups.map(group => group.id);
|
||||
const requestBody = { status_order: columnOrder };
|
||||
const response = await statusApiService.updateStatusOrder(requestBody, projectId);
|
||||
if (!response.done) {
|
||||
// Revert the change if API call fails
|
||||
const revertedGroups = [...reorderedGroups];
|
||||
const [movedBackGroup] = revertedGroups.splice(toIdx, 1);
|
||||
revertedGroups.splice(fromIdx, 0, movedBackGroup);
|
||||
dispatch(reorderGroups({ fromIndex: toIdx, toIndex: fromIdx, reorderedGroups: revertedGroups }));
|
||||
alertService.error('Failed to update column order', 'Please try again');
|
||||
}
|
||||
} catch (error) {
|
||||
// Revert the change if API call fails
|
||||
const revertedGroups = [...reorderedGroups];
|
||||
const [movedBackGroup] = revertedGroups.splice(toIdx, 1);
|
||||
revertedGroups.splice(fromIdx, 0, movedBackGroup);
|
||||
dispatch(reorderGroups({ fromIndex: toIdx, toIndex: fromIdx, reorderedGroups: revertedGroups }));
|
||||
alertService.error('Failed to update column order', 'Please try again');
|
||||
logger.error('Failed to update column order', error);
|
||||
}
|
||||
|
||||
setDraggedGroupId(null);
|
||||
setDragType(null);
|
||||
};
|
||||
|
||||
// Task drag handlers
|
||||
const handleTaskDragStart = (e: React.DragEvent, taskId: string, groupId: string) => {
|
||||
setDraggedTaskId(taskId);
|
||||
setDraggedTaskGroupId(groupId);
|
||||
setDragType('task');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
const handleTaskDragOver = (e: React.DragEvent, groupId: string, taskIdx: number) => {
|
||||
if (dragType !== 'task') return;
|
||||
e.preventDefault();
|
||||
if (draggedTaskId) {
|
||||
setHoveredGroupId(groupId);
|
||||
setHoveredTaskIdx(taskIdx);
|
||||
}
|
||||
};
|
||||
const handleTaskDrop = (e: React.DragEvent, targetGroupId: string, targetTaskIdx: number) => {
|
||||
if (dragType !== 'task') return;
|
||||
e.preventDefault();
|
||||
if (!draggedTaskId || !draggedTaskGroupId || hoveredGroupId === null || hoveredTaskIdx === null) return;
|
||||
|
||||
// Calculate new order and dispatch
|
||||
const sourceGroup = taskGroups.find(g => g.id === draggedTaskGroupId);
|
||||
const targetGroup = taskGroups.find(g => g.id === targetGroupId);
|
||||
if (!sourceGroup || !targetGroup) return;
|
||||
|
||||
const taskIdx = sourceGroup.tasks.findIndex(t => t.id === draggedTaskId);
|
||||
if (taskIdx === -1) return;
|
||||
|
||||
const movedTask = sourceGroup.tasks[taskIdx];
|
||||
let insertIdx = hoveredTaskIdx;
|
||||
|
||||
// Handle same group reordering
|
||||
if (sourceGroup.id === targetGroup.id) {
|
||||
// Create a single updated array for the same group
|
||||
const updatedTasks = [...sourceGroup.tasks];
|
||||
updatedTasks.splice(taskIdx, 1); // Remove from original position
|
||||
|
||||
// Adjust insert index if moving forward in the same array
|
||||
if (taskIdx < insertIdx) {
|
||||
insertIdx--;
|
||||
}
|
||||
|
||||
if (insertIdx < 0) insertIdx = 0;
|
||||
if (insertIdx > updatedTasks.length) insertIdx = updatedTasks.length;
|
||||
|
||||
updatedTasks.splice(insertIdx, 0, movedTask); // Insert at new position
|
||||
|
||||
dispatch(reorderTasks({
|
||||
activeGroupId: sourceGroup.id,
|
||||
overGroupId: targetGroup.id,
|
||||
fromIndex: taskIdx,
|
||||
toIndex: insertIdx,
|
||||
task: movedTask,
|
||||
updatedSourceTasks: updatedTasks,
|
||||
updatedTargetTasks: updatedTasks,
|
||||
}));
|
||||
dispatch(reorderEnhancedKanbanTasks({
|
||||
activeGroupId: sourceGroup.id,
|
||||
overGroupId: targetGroup.id,
|
||||
fromIndex: taskIdx,
|
||||
toIndex: insertIdx,
|
||||
task: movedTask,
|
||||
updatedSourceTasks: updatedTasks,
|
||||
updatedTargetTasks: updatedTasks,
|
||||
}) as any);
|
||||
} else {
|
||||
// Handle cross-group reordering
|
||||
const updatedSourceTasks = [...sourceGroup.tasks];
|
||||
updatedSourceTasks.splice(taskIdx, 1);
|
||||
|
||||
const updatedTargetTasks = [...targetGroup.tasks];
|
||||
if (insertIdx < 0) insertIdx = 0;
|
||||
if (insertIdx > updatedTargetTasks.length) insertIdx = updatedTargetTasks.length;
|
||||
updatedTargetTasks.splice(insertIdx, 0, movedTask);
|
||||
|
||||
dispatch(reorderTasks({
|
||||
activeGroupId: sourceGroup.id,
|
||||
overGroupId: targetGroup.id,
|
||||
fromIndex: taskIdx,
|
||||
toIndex: insertIdx,
|
||||
task: movedTask,
|
||||
updatedSourceTasks,
|
||||
updatedTargetTasks,
|
||||
}));
|
||||
dispatch(reorderEnhancedKanbanTasks({
|
||||
activeGroupId: sourceGroup.id,
|
||||
overGroupId: targetGroup.id,
|
||||
fromIndex: taskIdx,
|
||||
toIndex: insertIdx,
|
||||
task: movedTask,
|
||||
updatedSourceTasks,
|
||||
updatedTargetTasks,
|
||||
}) as any);
|
||||
}
|
||||
|
||||
// Socket emit for task order
|
||||
if (socket && projectId && teamId && movedTask) {
|
||||
let toSortOrder = -1;
|
||||
let toLastIndex = false;
|
||||
if (insertIdx === targetGroup.tasks.length) {
|
||||
toSortOrder = -1;
|
||||
toLastIndex = true;
|
||||
} else if (targetGroup.tasks[insertIdx]) {
|
||||
toSortOrder = typeof targetGroup.tasks[insertIdx].sort_order === 'number'
|
||||
? targetGroup.tasks[insertIdx].sort_order
|
||||
: -1;
|
||||
toLastIndex = false;
|
||||
} else if (targetGroup.tasks.length > 0) {
|
||||
const lastSortOrder = targetGroup.tasks[targetGroup.tasks.length - 1].sort_order;
|
||||
toSortOrder = typeof lastSortOrder === 'number' ? lastSortOrder : -1;
|
||||
toLastIndex = false;
|
||||
}
|
||||
socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), {
|
||||
project_id: projectId,
|
||||
from_index: movedTask.sort_order,
|
||||
to_index: toSortOrder,
|
||||
to_last_index: toLastIndex,
|
||||
from_group: sourceGroup.id,
|
||||
to_group: targetGroup.id,
|
||||
group_by: groupBy || 'status',
|
||||
task: movedTask,
|
||||
team_id: teamId,
|
||||
});
|
||||
}
|
||||
|
||||
setDraggedTaskId(null);
|
||||
setDraggedTaskGroupId(null);
|
||||
setHoveredGroupId(null);
|
||||
setHoveredTaskIdx(null);
|
||||
setDragType(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
|
||||
// Handler for new task received via socket
|
||||
const handleNewTaskReceived = (data: any) => {
|
||||
if (!data) return;
|
||||
if (data.parent_task_id) {
|
||||
// Subtask: update subtasks in the correct group
|
||||
dispatch({
|
||||
type: 'enhancedKanbanReducer/updateEnhancedKanbanSubtask',
|
||||
payload: { sectionId: '', subtask: data, mode: 'add' }
|
||||
});
|
||||
} else {
|
||||
// Regular task: add to the correct group
|
||||
let sectionId = '';
|
||||
if (groupBy === 'status') sectionId = data.status;
|
||||
else if (groupBy === 'priority') sectionId = data.priority;
|
||||
else if (groupBy === 'phase') sectionId = data.phase_id;
|
||||
dispatch({
|
||||
type: 'enhancedKanbanReducer/addTaskToGroup',
|
||||
payload: { sectionId, task: data }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
socket.on(SocketEvents.QUICK_TASK.toString(), handleNewTaskReceived);
|
||||
return () => {
|
||||
socket.off(SocketEvents.QUICK_TASK.toString(), handleNewTaskReceived);
|
||||
};
|
||||
}, [socket, groupBy, dispatch]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card>
|
||||
<Empty description={`Error loading tasks: ${error}`} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<React.Suspense fallback={<div>Loading filters...</div>}>
|
||||
<ImprovedTaskFilters position="board" />
|
||||
</React.Suspense>
|
||||
</div>
|
||||
<div className="enhanced-kanban-board">
|
||||
{loadingGroups ? (
|
||||
<Card>
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<Skeleton active />
|
||||
</div>
|
||||
</Card>
|
||||
) : taskGroups.length === 0 ? (
|
||||
<Card>
|
||||
<Empty description="No tasks found" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</Card>
|
||||
) : (
|
||||
<div className="kanban-groups-container">
|
||||
{taskGroups.map(group => (
|
||||
<KanbanGroup
|
||||
key={group.id}
|
||||
group={group}
|
||||
onGroupDragStart={handleGroupDragStart}
|
||||
onGroupDragOver={handleGroupDragOver}
|
||||
onGroupDrop={handleGroupDrop}
|
||||
onTaskDragStart={handleTaskDragStart}
|
||||
onTaskDragOver={handleTaskDragOver}
|
||||
onTaskDrop={handleTaskDrop}
|
||||
hoveredTaskIdx={hoveredGroupId === group.id ? hoveredTaskIdx : null}
|
||||
hoveredGroupId={hoveredGroupId}
|
||||
/>
|
||||
))}
|
||||
<EnhancedKanbanCreateSection />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnhancedKanbanBoardNativeDnD;
|
||||
@@ -0,0 +1,169 @@
|
||||
import React, { memo, useMemo, useState } from 'react';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { ITaskListGroup } from '@/types/tasks/taskList.types';
|
||||
import TaskCard from './TaskCard';
|
||||
import { themeWiseColor } from '@/utils/themeWiseColor';
|
||||
import EnhancedKanbanCreateTaskCard from '../EnhancedKanbanCreateTaskCard';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import Button from 'antd/es/button';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import useIsProjectManager from '@/hooks/useIsProjectManager';
|
||||
|
||||
interface KanbanGroupProps {
|
||||
group: ITaskListGroup;
|
||||
onGroupDragStart: (e: React.DragEvent, groupId: string) => void;
|
||||
onGroupDragOver: (e: React.DragEvent) => void;
|
||||
onGroupDrop: (e: React.DragEvent, groupId: string) => void;
|
||||
onTaskDragStart: (e: React.DragEvent, taskId: string, groupId: string) => void;
|
||||
onTaskDragOver: (e: React.DragEvent, groupId: string, taskIdx: number) => void;
|
||||
onTaskDrop: (e: React.DragEvent, groupId: string, taskIdx: number) => void;
|
||||
hoveredTaskIdx: number | null;
|
||||
hoveredGroupId: string | null;
|
||||
}
|
||||
|
||||
const KanbanGroup: React.FC<KanbanGroupProps> = memo(({
|
||||
group,
|
||||
onGroupDragStart,
|
||||
onGroupDragOver,
|
||||
onGroupDrop,
|
||||
onTaskDragStart,
|
||||
onTaskDragOver,
|
||||
onTaskDrop,
|
||||
hoveredTaskIdx,
|
||||
hoveredGroupId
|
||||
}) => {
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
const { t } = useTranslation('kanban-board');
|
||||
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
|
||||
const isProjectManager = useIsProjectManager();
|
||||
const [showNewCardTop, setShowNewCardTop] = useState(false);
|
||||
const [showNewCardBottom, setShowNewCardBottom] = useState(false);
|
||||
const headerBackgroundColor = useMemo(() => {
|
||||
if (themeMode === 'dark') {
|
||||
return group.color_code_dark || group.color_code || '#1e1e1e';
|
||||
}
|
||||
return group.color_code || '#f5f5f5';
|
||||
}, [themeMode, group.color_code, group.color_code_dark]);
|
||||
|
||||
return (
|
||||
<div className="enhanced-kanban-group">
|
||||
<div
|
||||
className="enhanced-kanban-group-header"
|
||||
style={{
|
||||
backgroundColor: headerBackgroundColor,
|
||||
}}
|
||||
draggable
|
||||
onDragStart={e => onGroupDragStart(e, group.id)}
|
||||
onDragOver={onGroupDragOver}
|
||||
onDrop={e => onGroupDrop(e, group.id)}
|
||||
>
|
||||
<h3>{group.name}</h3>
|
||||
<span className="task-count">{group.tasks.length}</span>
|
||||
</div>
|
||||
<div className="enhanced-kanban-group-tasks">
|
||||
{/* If group is empty, render a drop zone */}
|
||||
{group.tasks.length === 0 && (
|
||||
<div
|
||||
className="empty-drop-zone"
|
||||
style={{
|
||||
padding: 8,
|
||||
height: 500,
|
||||
background: themeWiseColor(
|
||||
'linear-gradient( 180deg, #fafafa, rgba(245, 243, 243, 0))',
|
||||
'linear-gradient( 180deg, #2a2b2d, rgba(42, 43, 45, 0))',
|
||||
themeMode
|
||||
),
|
||||
borderRadius: 6,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
paddingTop: 8,
|
||||
color: '#888',
|
||||
fontStyle: 'italic',
|
||||
}}
|
||||
onDragOver={e => { e.preventDefault(); onTaskDragOver(e, group.id, 0); }}
|
||||
onDrop={e => { e.preventDefault(); onTaskDrop(e, group.id, 0); }}
|
||||
>
|
||||
{/* Drop indicator at the end of the group */}
|
||||
{hoveredGroupId === group.id && hoveredTaskIdx === group.tasks.length && (
|
||||
<div className="drop-preview-indicator">
|
||||
<div className="drop-line" />
|
||||
</div>
|
||||
)}
|
||||
{(isOwnerOrAdmin || isProjectManager) && !showNewCardTop && !showNewCardBottom && (
|
||||
<Button
|
||||
type="text"
|
||||
style={{
|
||||
height: '38px',
|
||||
width: '100%',
|
||||
borderRadius: 6,
|
||||
boxShadow: 'none',
|
||||
}}
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
setShowNewCardBottom(false);
|
||||
setShowNewCardTop(true);
|
||||
}}
|
||||
>
|
||||
{t('addTask')}
|
||||
</Button>
|
||||
)}
|
||||
{showNewCardTop && <EnhancedKanbanCreateTaskCard sectionId={group.id} setShowNewCard={setShowNewCardTop} position='top' />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Drop indicator at the top of the group */}
|
||||
{hoveredGroupId === group.id && hoveredTaskIdx === 0 && (
|
||||
<div className="drop-preview-indicator">
|
||||
<div className="drop-line" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{group.tasks.map((task, idx) => (
|
||||
<TaskCard
|
||||
task={task}
|
||||
onTaskDragStart={onTaskDragStart}
|
||||
onTaskDragOver={onTaskDragOver}
|
||||
onTaskDrop={onTaskDrop}
|
||||
groupId={group.id}
|
||||
isDropIndicator={hoveredGroupId === group.id && hoveredTaskIdx === idx}
|
||||
idx={idx}
|
||||
/>
|
||||
))}
|
||||
{(isOwnerOrAdmin || isProjectManager) && !showNewCardTop && !showNewCardBottom && group.tasks.length > 0 && (
|
||||
<Button
|
||||
type="text"
|
||||
style={{
|
||||
height: '40px',
|
||||
width: '100%',
|
||||
borderRadius: 6,
|
||||
boxShadow: 'none',
|
||||
marginTop: 8,
|
||||
}}
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
setShowNewCardBottom(true);
|
||||
setShowNewCardTop(false);
|
||||
}}
|
||||
>
|
||||
{t('addTask')}
|
||||
</Button>
|
||||
)}
|
||||
{showNewCardBottom && <EnhancedKanbanCreateTaskCard sectionId={group.id} setShowNewCard={setShowNewCardBottom} position='bottom' />}
|
||||
|
||||
{/* Drop indicator at the end of the group */}
|
||||
{hoveredGroupId === group.id && hoveredTaskIdx === group.tasks.length && (
|
||||
<div className="drop-preview-indicator">
|
||||
<div className="drop-line" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
KanbanGroup.displayName = 'KanbanGroup';
|
||||
|
||||
export default KanbanGroup;
|
||||
@@ -0,0 +1,120 @@
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '@/app/store';
|
||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
import { setSelectedTaskId, setShowTaskDrawer } from '@/features/task-drawer/task-drawer.slice';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import AvatarGroup from '@/components/AvatarGroup';
|
||||
import LazyAssigneeSelectorWrapper from '@/components/task-management/lazy-assignee-selector';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
interface TaskCardProps {
|
||||
task: IProjectTask;
|
||||
onTaskDragStart: (e: React.DragEvent, taskId: string, groupId: string) => void;
|
||||
onTaskDragOver: (e: React.DragEvent, groupId: string, taskIdx: number) => void;
|
||||
onTaskDrop: (e: React.DragEvent, groupId: string, taskIdx: number) => void;
|
||||
groupId: string;
|
||||
isDropIndicator: boolean;
|
||||
idx: number;
|
||||
}
|
||||
|
||||
const TaskCard: React.FC<TaskCardProps> = memo(({
|
||||
task,
|
||||
onTaskDragStart,
|
||||
onTaskDragOver,
|
||||
onTaskDrop,
|
||||
groupId,
|
||||
isDropIndicator,
|
||||
idx
|
||||
}) => {
|
||||
const themeMode = useSelector((state: RootState) => state.themeReducer.mode);
|
||||
const background = themeMode === 'dark' ? '#23272f' : '#fff';
|
||||
const color = themeMode === 'dark' ? '#fff' : '#23272f';
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation('kanban-board');
|
||||
|
||||
const handleCardClick = useCallback((e: React.MouseEvent, id: string) => {
|
||||
// Prevent the event from propagating to parent elements
|
||||
e.stopPropagation();
|
||||
dispatch(setSelectedTaskId(id));
|
||||
dispatch(setShowTaskDrawer(true));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDropIndicator && (
|
||||
<div
|
||||
style={{
|
||||
height: 80,
|
||||
background: themeMode === 'dark' ? '#2a2a2a' : '#f0f0f0',
|
||||
borderRadius: 6,
|
||||
border: `5px`
|
||||
}}
|
||||
onDragStart={e => onTaskDragStart(e, task.id!, groupId)}
|
||||
onDragOver={e => onTaskDragOver(e, groupId, idx)}
|
||||
onDrop={e => onTaskDrop(e, groupId, idx)}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="enhanced-kanban-task-card"
|
||||
draggable
|
||||
onDragStart={e => onTaskDragStart(e, task.id!, groupId)}
|
||||
onDragOver={e => onTaskDragOver(e, groupId, idx)}
|
||||
onDrop={e => onTaskDrop(e, groupId, idx)}
|
||||
style={{ background, color }}
|
||||
onClick={e => handleCardClick(e, task.id!)}
|
||||
>
|
||||
<div className="task-content">
|
||||
<div className="task_labels" style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
|
||||
{task.labels?.map(label => (
|
||||
<div
|
||||
key={label.id}
|
||||
className="task-label"
|
||||
style={{
|
||||
backgroundColor: label.color_code,
|
||||
display: 'inline-block',
|
||||
borderRadius: '4px',
|
||||
padding: '2px 8px',
|
||||
color: '#fff',
|
||||
fontSize: 8,
|
||||
marginRight: 4,
|
||||
whiteSpace: 'nowrap',
|
||||
minWidth: 0
|
||||
}}
|
||||
>
|
||||
{label.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="task-content" style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: task.priority_color || '#d9d9d9' }}
|
||||
/>
|
||||
<div className="task-title" style={{ marginLeft: 8 }}>{task.name}</div>
|
||||
</div>
|
||||
|
||||
<div className="task-assignees-row" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
||||
<div className="task-due-date" style={{ fontSize: 10, color: '#888', marginRight: 8, whiteSpace: 'nowrap' }}>
|
||||
{task.end_date ? format(new Date(task.end_date), 'MMM d, yyyy') : ''}
|
||||
</div>
|
||||
<div className="task-assignees" style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<AvatarGroup
|
||||
members={task.names || []}
|
||||
maxCount={3}
|
||||
isDarkMode={themeMode === 'dark'}
|
||||
size={24}
|
||||
/>
|
||||
<LazyAssigneeSelectorWrapper task={task} groupId={groupId} isDarkMode={themeMode === 'dark'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
TaskCard.displayName = 'TaskCard';
|
||||
|
||||
export default TaskCard;
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default } from './EnhancedKanbanBoardNativeDnD';
|
||||
export { default as TaskCard } from './TaskCard';
|
||||
export { default as KanbanGroup } from './KanbanGroup';
|
||||
@@ -13,6 +13,12 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
html.light .enhanced-kanban-task-card {
|
||||
border: 1.5px solid #e1e4e8 !important; /* Asana-like light border */
|
||||
box-shadow: 0 1px 4px 0 rgba(60, 64, 67, 0.08), 0 0.5px 1.5px 0 rgba(60, 64, 67, 0.03);
|
||||
background: #fff !important;
|
||||
}
|
||||
|
||||
.enhanced-kanban-task-card:hover {
|
||||
box-shadow: 0 2px 6px var(--ant-color-shadow);
|
||||
transform: translateY(-1px);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import EnhancedKanbanBoard from '@/components/enhanced-kanban/EnhancedKanbanBoard';
|
||||
import EnhancedKanbanBoardNativeDnD from '@/components/enhanced-kanban/EnhancedKanbanBoardNativeDnD/EnhancedKanbanBoardNativeDnD';
|
||||
|
||||
const ProjectViewEnhancedBoard: React.FC = () => {
|
||||
const { project } = useAppSelector(state => state.projectReducer);
|
||||
@@ -11,7 +12,8 @@ const ProjectViewEnhancedBoard: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="project-view-enhanced-board">
|
||||
<EnhancedKanbanBoard projectId={project.id} />
|
||||
{/* <EnhancedKanbanBoard projectId={project.id} /> */}
|
||||
<EnhancedKanbanBoardNativeDnD projectId={project.id} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user