Merge pull request #237 from Worklenz/fix/task-drag-and-drop-improvement
Fix/task drag and drop improvement
This commit is contained in:
@@ -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]);
|
||||
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user