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

Fix/task drag and drop improvement
This commit is contained in:
Chamika J
2025-07-07 02:40:15 +05:30
committed by GitHub
43 changed files with 3527 additions and 850 deletions

View File

@@ -26,6 +26,9 @@ import { toggleField } from '@/features/task-management/taskListFields.slice';
import {
fetchTasksV3,
setSearch as setTaskManagementSearch,
setArchived as setTaskManagementArchived,
toggleArchived as toggleTaskManagementArchived,
selectArchived,
} from '@/features/task-management/task-management.slice';
import {
setCurrentGrouping,
@@ -443,11 +446,11 @@ const FilterDropdown: React.FC<{
${
selectedCount > 0
? isDarkMode
? 'bg-blue-600 text-white border-blue-500'
? 'bg-gray-600 text-white border-gray-500'
: 'bg-blue-50 text-blue-800 border-blue-300 font-semibold'
: `${themeClasses.buttonBg} ${themeClasses.buttonBorder} ${themeClasses.buttonText}`
}
hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2
${isDarkMode ? 'focus:ring-offset-gray-900' : 'focus:ring-offset-white'}
`}
aria-expanded={isOpen}
@@ -456,7 +459,7 @@ const FilterDropdown: React.FC<{
<IconComponent className="w-3.5 h-3.5" />
<span>{section.label}</span>
{selectedCount > 0 && (
<span className="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-blue-500 rounded-full">
<span className="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-gray-500 rounded-full">
{selectedCount}
</span>
)}
@@ -518,7 +521,7 @@ const FilterDropdown: React.FC<{
${
isSelected
? isDarkMode
? 'bg-blue-600 text-white'
? 'bg-gray-600 text-white'
: 'bg-blue-50 text-blue-800 font-semibold'
: `${themeClasses.optionText} ${themeClasses.optionHover}`
}
@@ -530,7 +533,7 @@ const FilterDropdown: React.FC<{
flex items-center justify-center w-3.5 h-3.5 border rounded
${
isSelected
? 'bg-blue-500 border-blue-500 text-white'
? 'bg-gray-600 border-gray-800 text-white'
: 'border-gray-300 dark:border-gray-600'
}
`}
@@ -730,7 +733,7 @@ const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({
${
visibleCount > 0
? isDarkMode
? 'bg-blue-600 text-white border-blue-500'
? 'bg-gray-600 text-white border-gray-500'
: 'bg-blue-50 text-blue-800 border-blue-300 font-semibold'
: `${themeClasses.buttonBg} ${themeClasses.buttonBorder} ${themeClasses.buttonText}`
}
@@ -743,7 +746,9 @@ const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({
<EyeOutlined className="w-3.5 h-3.5" />
<span>Fields</span>
{visibleCount > 0 && (
<span className="inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-blue-500 rounded-full">
<span
className={`inline-flex items-center justify-center w-4 h-4 text-xs font-bold ${isDarkMode ? 'text-white bg-gray-500' : 'text-gray-800 bg-gray-300'} rounded-full`}
>
{visibleCount}
</span>
)}
@@ -778,8 +783,8 @@ const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({
${
isSelected
? isDarkMode
? 'bg-blue-600 text-white'
: 'bg-blue-50 text-blue-800 font-semibold'
? 'text-white font-semibold'
: 'text-gray-800 font-semibold'
: `${themeClasses.optionText} ${themeClasses.optionHover}`
}
`}
@@ -790,7 +795,7 @@ const FieldsDropdown: React.FC<{ themeClasses: any; isDarkMode: boolean }> = ({
flex items-center justify-center w-3.5 h-3.5 border rounded
${
isSelected
? 'bg-blue-500 border-blue-500 text-white'
? 'bg-gray-600 border-gray-600 text-white'
: 'border-gray-300 dark:border-gray-600'
}
`}
@@ -826,13 +831,17 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({ position, cla
// Enhanced Kanban state
const kanbanState = useAppSelector((state: RootState) => state.enhancedKanbanReducer);
// Get archived state from the appropriate slice based on position
const taskManagementArchived = useAppSelector(selectArchived);
const taskReducerArchived = useAppSelector(state => state.taskReducer.archived);
const showArchived = position === 'list' ? taskManagementArchived : taskReducerArchived;
// Use the filter data loader hook
useFilterDataLoader();
// Local state for filter sections
const [filterSections, setFilterSections] = useState<FilterSection[]>([]);
const [searchValue, setSearchValue] = useState('');
const [showArchived, setShowArchived] = useState(false);
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
const [activeFiltersCount, setActiveFiltersCount] = useState(0);
const [clearingFilters, setClearingFilters] = useState(false);
@@ -1077,7 +1086,6 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({ position, cla
const batchUpdates = () => {
// Clear local state immediately for UI feedback
setSearchValue('');
setShowArchived(false);
// Update local filter sections state immediately
setFilterSections(prev =>
@@ -1116,6 +1124,13 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({ position, cla
// Clear priority filters
dispatch(setPriorities([]));
// Clear archived state based on position
if (position === 'list') {
dispatch(setTaskManagementArchived(false));
} else {
dispatch(setKanbanArchived(false));
}
};
// Execute Redux updates
@@ -1137,14 +1152,17 @@ const ImprovedTaskFilters: React.FC<ImprovedTaskFiltersProps> = ({ position, cla
}, [projectId, projectView, dispatch, currentTaskLabels, currentTaskAssignees, clearingFilters]);
const toggleArchived = useCallback(() => {
setShowArchived(!showArchived);
if (position === 'board') {
dispatch(setKanbanArchived(!showArchived));
if (projectId) {
dispatch(fetchEnhancedKanbanGroups(projectId));
}
} else {
// ... existing logic ...
// For TaskListV2, use the task management slice
dispatch(toggleTaskManagementArchived());
if (projectId) {
dispatch(fetchTasksV3(projectId));
}
}
}, [dispatch, projectId, position, showArchived]);

View File

@@ -18,43 +18,21 @@ import { Card, Spin, Empty, Alert } from 'antd';
import { RootState } from '@/app/store';
import {
selectAllTasks,
selectGroups,
selectGrouping,
selectLoading,
selectError,
selectSelectedPriorities,
selectSearch,
reorderTasks,
moveTaskToGroup,
moveTaskBetweenGroups,
optimisticTaskMove,
reorderTasksInGroup,
setLoading,
setError,
setSelectedPriorities,
setSearch,
resetTaskManagement,
toggleTaskExpansion,
addSubtaskToParent,
fetchTasksV3,
selectTaskGroupsV3,
fetchSubTasks,
} from '@/features/task-management/task-management.slice';
import {
selectCurrentGrouping,
selectCollapsedGroups,
selectIsGroupCollapsed,
toggleGroupCollapsed,
expandAllGroups,
collapseAllGroups,
} from '@/features/task-management/grouping.slice';
import {
selectSelectedTaskIds,
selectLastSelectedTaskId,
selectIsTaskSelected,
selectTask,
deselectTask,
toggleTaskSelection,
selectRange,
clearSelection,
selectTask,
} from '@/features/task-management/selection.slice';
import {
selectTasks,
@@ -89,18 +67,11 @@ import {
IBulkTasksPriorityChangeRequest,
IBulkTasksStatusChangeRequest,
} from '@/types/tasks/bulk-action-bar.types';
import { ITaskStatus } from '@/types/tasks/taskStatus.types';
import { ITaskPriority } from '@/types/tasks/taskPriority.types';
import { ITaskPhase } from '@/types/tasks/taskPhase.types';
import { ITaskLabel } from '@/types/tasks/taskLabel.types';
import { ITeamMemberViewModel } from '@/types/teamMembers/teamMembersGetResponse.types';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status';
import alertService from '@/services/alerts/alertService';
import logger from '@/utils/errorLogger';
import { fetchLabels } from '@/features/taskAttributes/taskLabelSlice';
import { performanceMonitor } from '@/utils/performance-monitor';
import debugPerformance from '@/utils/debug-performance';
// Import the improved TaskListFilters component synchronously to avoid suspense
import ImprovedTaskFilters from './improved-task-filters';
@@ -173,18 +144,11 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
// Redux selectors using V3 API (pre-processed data, minimal loops)
const tasks = useSelector(selectAllTasks);
const groups = useSelector(selectGroups);
const grouping = useSelector(selectGrouping);
const loading = useSelector(selectLoading);
const error = useSelector(selectError);
const selectedPriorities = useSelector(selectSelectedPriorities);
const searchQuery = useSelector(selectSearch);
const taskGroups = useSelector(selectTaskGroupsV3, shallowEqual);
const currentGrouping = useSelector(selectCurrentGrouping);
const collapsedGroups = useSelector(selectCollapsedGroups);
const selectedTaskIds = useSelector(selectSelectedTaskIds);
const lastSelectedTaskId = useSelector(selectLastSelectedTaskId);
const selectedTasks = useSelector((state: RootState) => state.bulkActionReducer.selectedTasks);
// Bulk action selectors
const statusList = useSelector((state: RootState) => state.taskStatusReducer.status);
@@ -202,9 +166,11 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const tasksById = useMemo(() => {
const map: Record<string, Task> = {};
// Cache all tasks for full functionality - performance optimizations are handled at the virtualization level
tasks.forEach(task => {
map[task.id] = task;
});
if (Array.isArray(tasks)) {
tasks.forEach((task: Task) => {
map[task.id] = task;
});
}
return map;
}, [tasks]);
@@ -262,14 +228,6 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const hasAnyTasks = useMemo(() => totalTasks > 0, [totalTasks]);
// Memoized handlers for better performance
const handleGroupingChange = useCallback(
(newGroupBy: 'status' | 'priority' | 'phase') => {
dispatch(setCurrentGrouping(newGroupBy));
},
[dispatch]
);
// Add isDragging state
const [isDragging, setIsDragging] = useState(false);
@@ -280,7 +238,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const taskId = active.id as string;
// Find the task and its group
const activeTask = tasks.find(t => t.id === taskId) || null;
const activeTask = Array.isArray(tasks) ? tasks.find((t: Task) => t.id === taskId) || null : null;
let activeGroupId: string | null = null;
if (activeTask) {
@@ -312,7 +270,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const overId = over.id as string;
// Check if we're hovering over a task or a group container
const targetTask = tasks.find(t => t.id === overId);
const targetTask = Array.isArray(tasks) ? tasks.find((t: Task) => t.id === overId) : undefined;
let targetGroupId = overId;
if (targetTask) {
@@ -362,7 +320,7 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
let targetIndex = -1;
// Check if dropping on a task or a group
const targetTask = tasks.find(t => t.id === overId);
const targetTask = Array.isArray(tasks) ? tasks.find((t: Task) => t.id === overId) : undefined;
if (targetTask) {
// Dropping on a task, find which group contains this task
for (const group of taskGroups) {
@@ -398,13 +356,10 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
// Use the new reorderTasksInGroup action that properly handles group arrays
dispatch(
reorderTasksInGroup({
taskId: activeTaskId,
fromGroupId: currentDragState.activeGroupId,
toGroupId: targetGroupId,
fromIndex: sourceIndex,
toIndex: finalTargetIndex,
groupType: targetGroup.groupType,
groupValue: targetGroup.groupValue,
sourceTaskId: activeTaskId,
destinationTaskId: targetTask?.id || '',
sourceGroupId: currentDragState.activeGroupId,
destinationGroupId: targetGroupId,
})
);
@@ -448,10 +403,10 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
const newSelectedIds = Array.from(currentSelectedIds);
// Map selected tasks to the required format
const newSelectedTasks = tasks
.filter((t) => newSelectedIds.includes(t.id))
const newSelectedTasks = Array.isArray(tasks) ? tasks
.filter((t: Task) => newSelectedIds.includes(t.id))
.map(
(task): IProjectTask => ({
(task: Task): IProjectTask => ({
id: task.id,
name: task.title,
task_key: task.task_key,
@@ -463,11 +418,11 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
description: task.description,
start_date: task.startDate,
end_date: task.dueDate,
total_hours: task.timeTracking.estimated || 0,
total_minutes: task.timeTracking.logged || 0,
total_hours: task.timeTracking?.estimated || 0,
total_minutes: task.timeTracking?.logged || 0,
progress: task.progress,
sub_tasks_count: task.sub_tasks_count || 0,
assignees: task.assignees.map((assigneeId) => ({
assignees: task.assignees?.map((assigneeId: string) => ({
id: assigneeId,
name: '',
email: '',
@@ -477,15 +432,16 @@ const TaskListBoard: React.FC<TaskListBoardProps> = ({ projectId, className = ''
})),
labels: task.labels,
manual_progress: false,
created_at: task.createdAt,
updated_at: task.updatedAt,
created_at: (task as any).createdAt || (task as any).created_at,
updated_at: (task as any).updatedAt || (task as any).updated_at,
sort_order: task.order,
})
);
) : [];
// Dispatch both actions to update the Redux state
dispatch(selectTasks(newSelectedTasks));
dispatch(selectTaskIds(newSelectedIds));
// Update selection state with the new task IDs
newSelectedIds.forEach(taskId => dispatch(selectTask(taskId)));
},
[dispatch, selectedTaskIds, tasks]
);