Merge pull request #230 from Worklenz/fix/task-drag-and-drop-improvement

Fix/task drag and drop improvement
This commit is contained in:
Chamika J
2025-07-04 09:00:38 +05:30
committed by GitHub
6 changed files with 64 additions and 54 deletions

View File

@@ -1,5 +1,6 @@
import React, { useMemo, useCallback } from 'react'; import React, { useMemo, useCallback } from 'react';
import { useDroppable } from '@dnd-kit/core'; import { useDroppable } from '@dnd-kit/core';
// @ts-ignore: Heroicons module types
import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
import { Checkbox } from 'antd'; import { Checkbox } from 'antd';
import { getContrastColor } from '@/utils/colorUtils'; import { getContrastColor } from '@/utils/colorUtils';
@@ -78,7 +79,7 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
return ( return (
<div <div
ref={setNodeRef} ref={setNodeRef}
className={`flex items-center px-4 py-2 cursor-pointer hover:opacity-80 transition-opacity duration-200 ease-in-out border-b border-gray-200 dark:border-gray-700 ${ className={`inline-flex w-max items-center px-4 py-2 cursor-pointer hover:opacity-80 transition-opacity duration-200 ease-in-out border-b border-gray-200 dark:border-gray-700 rounded-t-md ${
isOver ? 'ring-2 ring-blue-400 ring-opacity-50' : '' isOver ? 'ring-2 ring-blue-400 ring-opacity-50' : ''
}`} }`}
style={{ style={{
@@ -127,15 +128,9 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
{/* Color indicator (removed as full header is colored) */} {/* Color indicator (removed as full header is colored) */}
{/* Group name and count */} {/* Group name and count */}
<div className="flex items-center justify-between flex-1"> <div className="flex items-center flex-1">
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{group.name} {group.name} ({group.count})
</span>
<span
className="text-xs font-medium px-2 py-0.5 rounded-full"
style={{ backgroundColor: getContrastColor(headerTextColor) === '#000000' ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.2)', color: headerTextColor }}
>
{group.count}
</span> </span>
</div> </div>
</div> </div>

View File

@@ -55,7 +55,7 @@ import { TaskListField } from '@/types/task-list-field.types';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import ImprovedTaskFilters from '@/components/task-management/improved-task-filters'; import ImprovedTaskFilters from '@/components/task-management/improved-task-filters';
import OptimizedBulkActionBar from '@/components/task-management/optimized-bulk-action-bar'; import OptimizedBulkActionBar from '@/components/task-management/optimized-bulk-action-bar';
import { Bars3Icon } from '@heroicons/react/24/outline'; import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
import { HolderOutlined } from '@ant-design/icons'; import { HolderOutlined } from '@ant-design/icons';
import { COLUMN_KEYS } from '@/features/tasks/tasks.slice'; import { COLUMN_KEYS } from '@/features/tasks/tasks.slice';
@@ -137,6 +137,9 @@ const TaskListV2: React.FC<TaskListV2Props> = ({ projectId }) => {
const fields = useAppSelector(state => state.taskManagementFields) || []; const fields = useAppSelector(state => state.taskManagementFields) || [];
// Enable real-time updates via socket handlers
useTaskSocketHandlers();
// Filter visible columns based on fields // Filter visible columns based on fields
const visibleColumns = useMemo(() => { const visibleColumns = useMemo(() => {
return BASE_COLUMNS.filter(column => { return BASE_COLUMNS.filter(column => {
@@ -298,22 +301,13 @@ const TaskListV2: React.FC<TaskListV2Props> = ({ projectId }) => {
targetGroupId: targetGroup.id, targetGroupId: targetGroup.id,
})); }));
// If we need to insert at a specific position (not at the end) // Reorder task within target group at drop position
if (insertIndex < targetGroup.taskIds.length) {
const newTaskIds = [...targetGroup.taskIds];
// Remove the task if it was already added at the end
const taskIndex = newTaskIds.indexOf(activeId as string);
if (taskIndex > -1) {
newTaskIds.splice(taskIndex, 1);
}
// Insert at the correct position
newTaskIds.splice(insertIndex, 0, activeId as string);
dispatch(reorderTasksInGroup({ dispatch(reorderTasksInGroup({
taskIds: newTaskIds, sourceTaskId: activeId as string,
groupId: targetGroup.id, destinationTaskId: over.id as string,
sourceGroupId: activeGroup.id,
destinationGroupId: targetGroup.id,
})); }));
}
} else { } else {
// Reordering within the same group // Reordering within the same group
console.log('Reordering task within same group:', { console.log('Reordering task within same group:', {
@@ -324,15 +318,12 @@ const TaskListV2: React.FC<TaskListV2Props> = ({ projectId }) => {
}); });
if (activeIndex !== insertIndex) { if (activeIndex !== insertIndex) {
const newTaskIds = [...activeGroup.taskIds]; // Reorder task within same group at drop position
// Remove task from old position
newTaskIds.splice(activeIndex, 1);
// Insert at new position
newTaskIds.splice(insertIndex, 0, activeId as string);
dispatch(reorderTasksInGroup({ dispatch(reorderTasksInGroup({
taskIds: newTaskIds, sourceTaskId: activeId as string,
groupId: activeGroup.id, destinationTaskId: over.id as string,
sourceGroupId: activeGroup.id,
destinationGroupId: activeGroup.id,
})); }));
} }
} }
@@ -470,7 +461,7 @@ const TaskListV2: React.FC<TaskListV2Props> = ({ projectId }) => {
const isGroupEmpty = group.count === 0; const isGroupEmpty = group.count === 0;
return ( return (
<div> <div className={groupIndex > 0 ? 'mt-2' : ''}>
<TaskGroupHeader <TaskGroupHeader
group={{ group={{
id: group.id, id: group.id,
@@ -497,6 +488,7 @@ const TaskListV2: React.FC<TaskListV2Props> = ({ projectId }) => {
return ( return (
<TaskRow <TaskRow
taskId={task.id} taskId={task.id}
projectId={projectId}
visibleColumns={visibleColumns} visibleColumns={visibleColumns}
/> />
); );

View File

@@ -13,6 +13,8 @@ import { ClockIcon } from '@heroicons/react/24/outline';
import AvatarGroup from '../AvatarGroup'; import AvatarGroup from '../AvatarGroup';
import { DEFAULT_TASK_NAME } from '@/shared/constants'; import { DEFAULT_TASK_NAME } from '@/shared/constants';
import TaskProgress from '@/pages/projects/project-view-1/taskList/taskListTable/taskListTableCells/TaskProgress'; import TaskProgress from '@/pages/projects/project-view-1/taskList/taskListTable/taskListTableCells/TaskProgress';
import TaskStatusDropdown from '@/components/task-management/task-status-dropdown';
import TaskPriorityDropdown from '@/components/task-management/task-priority-dropdown';
import { useAppSelector } from '@/hooks/useAppSelector'; import { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppDispatch } from '@/hooks/useAppDispatch';
import { selectTaskById } from '@/features/task-management/task-management.slice'; import { selectTaskById } from '@/features/task-management/task-management.slice';
@@ -20,6 +22,7 @@ import { selectIsTaskSelected, toggleTaskSelection } from '@/features/task-manag
interface TaskRowProps { interface TaskRowProps {
taskId: string; taskId: string;
projectId: string;
visibleColumns: Array<{ visibleColumns: Array<{
id: string; id: string;
width: string; width: string;
@@ -47,7 +50,7 @@ const formatDate = (dateString: string): string => {
// Memoized date formatter to avoid repeated date parsing // Memoized date formatter to avoid repeated date parsing
const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, visibleColumns }) => { const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, projectId, visibleColumns }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const task = useAppSelector(state => selectTaskById(state, taskId)); const task = useAppSelector(state => selectTaskById(state, taskId));
const isSelected = useAppSelector(state => selectIsTaskSelected(state, taskId)); const isSelected = useAppSelector(state => selectIsTaskSelected(state, taskId));
@@ -207,12 +210,11 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, visibleColumns }) => {
case 'status': case 'status':
return ( return (
<div style={baseStyle}> <div style={baseStyle}>
<span <TaskStatusDropdown
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium" task={task}
style={statusStyle} projectId={projectId}
> isDarkMode={isDarkMode}
{task.status} />
</span>
</div> </div>
); );
@@ -236,12 +238,11 @@ const TaskRow: React.FC<TaskRowProps> = memo(({ taskId, visibleColumns }) => {
case 'priority': case 'priority':
return ( return (
<div style={baseStyle}> <div style={baseStyle}>
<span <TaskPriorityDropdown
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium" task={task}
style={priorityStyle} projectId={projectId}
> isDarkMode={isDarkMode}
{task.priority} />
</span>
</div> </div>
); );

View File

@@ -66,11 +66,18 @@ const TaskPriorityDropdown: React.FC<TaskPriorityDropdownProps> = ({
}; };
if (isOpen && buttonRef.current) { if (isOpen && buttonRef.current) {
// Calculate position // Calculate position with better handling of scrollable containers
const rect = buttonRef.current.getBoundingClientRect(); const rect = buttonRef.current.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const dropdownHeight = 200; // Estimated dropdown height
// Check if dropdown would go below viewport
const spaceBelow = viewportHeight - rect.bottom;
const shouldShowAbove = spaceBelow < dropdownHeight && rect.top > dropdownHeight;
setDropdownPosition({ setDropdownPosition({
top: rect.bottom + window.scrollY + 4, top: shouldShowAbove ? rect.top - dropdownHeight - 4 : rect.bottom + 4,
left: rect.left + window.scrollX, left: rect.left,
}); });
document.addEventListener('mousedown', handleClickOutside); document.addEventListener('mousedown', handleClickOutside);

View File

@@ -73,11 +73,18 @@ const TaskStatusDropdown: React.FC<TaskStatusDropdownProps> = ({
}; };
if (isOpen && buttonRef.current) { if (isOpen && buttonRef.current) {
// Calculate position // Calculate position with better handling of scrollable containers
const rect = buttonRef.current.getBoundingClientRect(); const rect = buttonRef.current.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const dropdownHeight = 200; // Estimated dropdown height
// Check if dropdown would go below viewport
const spaceBelow = viewportHeight - rect.bottom;
const shouldShowAbove = spaceBelow < dropdownHeight && rect.top > dropdownHeight;
setDropdownPosition({ setDropdownPosition({
top: rect.bottom + window.scrollY + 4, top: shouldShowAbove ? rect.top - dropdownHeight - 4 : rect.bottom + 4,
left: rect.left + window.scrollX, left: rect.left,
}); });
document.addEventListener('mousedown', handleClickOutside); document.addEventListener('mousedown', handleClickOutside);

View File

@@ -0,0 +1,8 @@
import { ComponentType, SVGProps } from 'react';
declare module '@heroicons/react/24/outline' {
export const ChevronDownIcon: ComponentType<SVGProps<SVGSVGElement>>;
export const ChevronRightIcon: ComponentType<SVGProps<SVGSVGElement>>;
export const Bars3Icon: ComponentType<SVGProps<SVGSVGElement>>;
export const ClockIcon: ComponentType<SVGProps<SVGSVGElement>>;
}