feat(task-management): enhance task list with improved drag-and-drop functionality and visual feedback
- Integrated droppable areas in TaskGroupHeader for better task organization. - Implemented drag-over visual feedback to indicate valid drop zones. - Enhanced TaskListV2 to handle cross-group task movement and reordering. - Optimized TaskRow for better rendering performance and added memoization for task details. - Improved Redux state management for task reordering and group handling.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useDroppable } from '@dnd-kit/core';
|
||||||
import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
|
import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
|
||||||
import { getContrastColor } from '@/utils/colorUtils';
|
import { getContrastColor } from '@/utils/colorUtils';
|
||||||
|
|
||||||
@@ -17,11 +18,23 @@ const TaskGroupHeader: React.FC<TaskGroupHeaderProps> = ({ group, isCollapsed, o
|
|||||||
const headerBackgroundColor = group.color || '#F0F0F0'; // Default light gray if no color
|
const headerBackgroundColor = group.color || '#F0F0F0'; // Default light gray if no color
|
||||||
const headerTextColor = getContrastColor(headerBackgroundColor);
|
const headerTextColor = getContrastColor(headerBackgroundColor);
|
||||||
|
|
||||||
|
// Make the group header droppable
|
||||||
|
const { isOver, setNodeRef } = useDroppable({
|
||||||
|
id: group.id,
|
||||||
|
data: {
|
||||||
|
type: 'group',
|
||||||
|
group,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
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"
|
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 ${
|
||||||
|
isOver ? 'ring-2 ring-blue-400 ring-opacity-50' : ''
|
||||||
|
}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: headerBackgroundColor,
|
backgroundColor: isOver ? `${headerBackgroundColor}dd` : headerBackgroundColor,
|
||||||
color: headerTextColor,
|
color: headerTextColor,
|
||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
top: 0,
|
top: 0,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { GroupedVirtuoso } from 'react-virtuoso';
|
|||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
|
DragOverEvent,
|
||||||
DragOverlay,
|
DragOverlay,
|
||||||
DragStartEvent,
|
DragStartEvent,
|
||||||
PointerSensor,
|
PointerSensor,
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
useSensors,
|
useSensors,
|
||||||
KeyboardSensor,
|
KeyboardSensor,
|
||||||
TouchSensor,
|
TouchSensor,
|
||||||
|
closestCenter,
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
import {
|
import {
|
||||||
SortableContext,
|
SortableContext,
|
||||||
@@ -27,6 +29,8 @@ import {
|
|||||||
selectSelectedPriorities,
|
selectSelectedPriorities,
|
||||||
selectSearch,
|
selectSearch,
|
||||||
fetchTasksV3,
|
fetchTasksV3,
|
||||||
|
reorderTasksInGroup,
|
||||||
|
moveTaskBetweenGroups,
|
||||||
} from '@/features/task-management/task-management.slice';
|
} from '@/features/task-management/task-management.slice';
|
||||||
import {
|
import {
|
||||||
selectCurrentGrouping,
|
selectCurrentGrouping,
|
||||||
@@ -57,7 +61,7 @@ import { COLUMN_KEYS } from '@/features/tasks/tasks.slice';
|
|||||||
// Base column configuration
|
// Base column configuration
|
||||||
const BASE_COLUMNS = [
|
const BASE_COLUMNS = [
|
||||||
{ id: 'dragHandle', label: '', width: '32px', isSticky: true, key: 'dragHandle' },
|
{ id: 'dragHandle', label: '', width: '32px', isSticky: true, key: 'dragHandle' },
|
||||||
{ id: 'taskKey', label: 'Key', width: '100px', isSticky: true, key: COLUMN_KEYS.KEY },
|
{ id: 'taskKey', label: 'Key', width: '100px', key: COLUMN_KEYS.KEY },
|
||||||
{ id: 'title', label: 'Title', width: '300px', isSticky: true, key: COLUMN_KEYS.NAME },
|
{ id: 'title', label: 'Title', width: '300px', isSticky: true, key: COLUMN_KEYS.NAME },
|
||||||
{ id: 'status', label: 'Status', width: '120px', key: COLUMN_KEYS.STATUS },
|
{ id: 'status', label: 'Status', width: '120px', key: COLUMN_KEYS.STATUS },
|
||||||
{ id: 'assignees', label: 'Assignees', width: '150px', key: COLUMN_KEYS.ASSIGNEES },
|
{ id: 'assignees', label: 'Assignees', width: '150px', key: COLUMN_KEYS.ASSIGNEES },
|
||||||
@@ -133,9 +137,9 @@ const TaskListV2: React.FC<TaskListV2Props> = ({ projectId }) => {
|
|||||||
// 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 => {
|
||||||
// Always show drag handle, task key, and title
|
// Always show drag handle and title (sticky columns)
|
||||||
if (column.isSticky) return true;
|
if (column.isSticky) return true;
|
||||||
// Check if field is visible
|
// Check if field is visible for all other columns (including task key)
|
||||||
const field = fields.find(f => f.key === column.key);
|
const field = fields.find(f => f.key === column.key);
|
||||||
return field?.visible ?? false;
|
return field?.visible ?? false;
|
||||||
});
|
});
|
||||||
@@ -176,6 +180,42 @@ const TaskListV2: React.FC<TaskListV2Props> = ({ projectId }) => {
|
|||||||
setActiveId(event.active.id as string);
|
setActiveId(event.active.id as string);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleDragOver = useCallback((event: DragOverEvent) => {
|
||||||
|
const { active, over } = event;
|
||||||
|
|
||||||
|
if (!over) return;
|
||||||
|
|
||||||
|
const activeId = active.id;
|
||||||
|
const overId = over.id;
|
||||||
|
|
||||||
|
// Find the active task and the item being dragged over
|
||||||
|
const activeTask = allTasks.find(task => task.id === activeId);
|
||||||
|
if (!activeTask) return;
|
||||||
|
|
||||||
|
// Check if we're dragging over a task or a group
|
||||||
|
const overTask = allTasks.find(task => task.id === overId);
|
||||||
|
const overGroup = groups.find(group => group.id === overId);
|
||||||
|
|
||||||
|
// Find the groups
|
||||||
|
const activeGroup = groups.find(group => group.taskIds.includes(activeTask.id));
|
||||||
|
let targetGroup = overGroup;
|
||||||
|
|
||||||
|
if (overTask) {
|
||||||
|
targetGroup = groups.find(group => group.taskIds.includes(overTask.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!activeGroup || !targetGroup) return;
|
||||||
|
|
||||||
|
// If dragging to a different group, we need to handle cross-group movement
|
||||||
|
if (activeGroup.id !== targetGroup.id) {
|
||||||
|
console.log('Cross-group drag detected:', {
|
||||||
|
activeTask: activeTask.id,
|
||||||
|
fromGroup: activeGroup.id,
|
||||||
|
toGroup: targetGroup.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [allTasks, groups]);
|
||||||
|
|
||||||
const handleDragEnd = useCallback((event: DragEndEvent) => {
|
const handleDragEnd = useCallback((event: DragEndEvent) => {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
setActiveId(null);
|
setActiveId(null);
|
||||||
@@ -184,47 +224,115 @@ const TaskListV2: React.FC<TaskListV2Props> = ({ projectId }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const activeId = active.id;
|
||||||
|
const overId = over.id;
|
||||||
|
|
||||||
// Find the active task
|
// Find the active task
|
||||||
const activeTask = allTasks.find(task => task.id === active.id);
|
const activeTask = allTasks.find(task => task.id === activeId);
|
||||||
if (!activeTask) {
|
if (!activeTask) {
|
||||||
console.error('Active task not found:', active.id);
|
console.error('Active task not found:', activeId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find which group the task is being moved to
|
// Find the groups
|
||||||
const overTask = allTasks.find(task => task.id === over.id);
|
|
||||||
if (!overTask) {
|
|
||||||
console.error('Over task not found:', over.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the groups for both tasks
|
|
||||||
const activeGroup = groups.find(group => group.taskIds.includes(activeTask.id));
|
const activeGroup = groups.find(group => group.taskIds.includes(activeTask.id));
|
||||||
const overGroup = groups.find(group => group.taskIds.includes(overTask.id));
|
if (!activeGroup) {
|
||||||
|
console.error('Could not find active group for task:', activeId);
|
||||||
if (!activeGroup || !overGroup) {
|
|
||||||
console.error('Could not find groups for tasks');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate new positions
|
// Check if we're dropping on a task or a group
|
||||||
const activeIndex = allTasks.findIndex(task => task.id === active.id);
|
const overTask = allTasks.find(task => task.id === overId);
|
||||||
const overIndex = allTasks.findIndex(task => task.id === over.id);
|
const overGroup = groups.find(group => group.id === overId);
|
||||||
|
|
||||||
|
let targetGroup = overGroup;
|
||||||
|
let insertIndex = 0;
|
||||||
|
|
||||||
|
if (overTask) {
|
||||||
|
// Dropping on a task
|
||||||
|
targetGroup = groups.find(group => group.taskIds.includes(overTask.id));
|
||||||
|
if (targetGroup) {
|
||||||
|
insertIndex = targetGroup.taskIds.indexOf(overTask.id);
|
||||||
|
}
|
||||||
|
} else if (overGroup) {
|
||||||
|
// Dropping on a group (at the end)
|
||||||
|
targetGroup = overGroup;
|
||||||
|
insertIndex = targetGroup.taskIds.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetGroup) {
|
||||||
|
console.error('Could not find target group');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCrossGroup = activeGroup.id !== targetGroup.id;
|
||||||
|
const activeIndex = activeGroup.taskIds.indexOf(activeTask.id);
|
||||||
|
|
||||||
console.log('Drag operation:', {
|
console.log('Drag operation:', {
|
||||||
activeId: active.id,
|
activeId,
|
||||||
overId: over.id,
|
overId,
|
||||||
activeIndex,
|
activeTask: activeTask.name || activeTask.title,
|
||||||
overIndex,
|
|
||||||
activeGroup: activeGroup.id,
|
activeGroup: activeGroup.id,
|
||||||
overGroup: overGroup.id,
|
targetGroup: targetGroup.id,
|
||||||
|
activeIndex,
|
||||||
|
insertIndex,
|
||||||
|
isCrossGroup,
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Implement the actual reordering logic
|
if (isCrossGroup) {
|
||||||
// This would typically involve:
|
// Moving task between groups
|
||||||
// 1. Updating the task order in Redux
|
console.log('Moving task between groups:', {
|
||||||
// 2. Sending the update to the backend
|
task: activeTask.name || activeTask.title,
|
||||||
// 3. Optimistic UI updates
|
from: activeGroup.title,
|
||||||
|
to: targetGroup.title,
|
||||||
|
newPosition: insertIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Move task to the target group
|
||||||
|
dispatch(moveTaskBetweenGroups({
|
||||||
|
taskId: activeId as string,
|
||||||
|
sourceGroupId: activeGroup.id,
|
||||||
|
targetGroupId: targetGroup.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// If we need to insert at a specific position (not at the end)
|
||||||
|
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({
|
||||||
|
taskIds: newTaskIds,
|
||||||
|
groupId: targetGroup.id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Reordering within the same group
|
||||||
|
console.log('Reordering task within same group:', {
|
||||||
|
task: activeTask.name || activeTask.title,
|
||||||
|
group: activeGroup.title,
|
||||||
|
from: activeIndex,
|
||||||
|
to: insertIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (activeIndex !== insertIndex) {
|
||||||
|
const newTaskIds = [...activeGroup.taskIds];
|
||||||
|
// Remove task from old position
|
||||||
|
newTaskIds.splice(activeIndex, 1);
|
||||||
|
// Insert at new position
|
||||||
|
newTaskIds.splice(insertIndex, 0, activeId as string);
|
||||||
|
|
||||||
|
dispatch(reorderTasksInGroup({
|
||||||
|
taskIds: newTaskIds,
|
||||||
|
groupId: activeGroup.id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}, [allTasks, groups]);
|
}, [allTasks, groups]);
|
||||||
|
|
||||||
@@ -233,7 +341,14 @@ const TaskListV2: React.FC<TaskListV2Props> = ({ projectId }) => {
|
|||||||
let currentTaskIndex = 0;
|
let currentTaskIndex = 0;
|
||||||
return groups.map(group => {
|
return groups.map(group => {
|
||||||
const isCurrentGroupCollapsed = collapsedGroups.has(group.id);
|
const isCurrentGroupCollapsed = collapsedGroups.has(group.id);
|
||||||
const visibleTasksInGroup = isCurrentGroupCollapsed ? [] : allTasks.filter(task => group.taskIds.includes(task.id));
|
|
||||||
|
// Order tasks according to group.taskIds array to maintain proper order
|
||||||
|
const visibleTasksInGroup = isCurrentGroupCollapsed
|
||||||
|
? []
|
||||||
|
: group.taskIds
|
||||||
|
.map(taskId => allTasks.find(task => task.id === taskId))
|
||||||
|
.filter((task): task is Task => task !== undefined); // Type guard to filter out undefined tasks
|
||||||
|
|
||||||
const tasksForVirtuoso = visibleTasksInGroup.map(task => ({
|
const tasksForVirtuoso = visibleTasksInGroup.map(task => ({
|
||||||
...task,
|
...task,
|
||||||
originalIndex: allTasks.indexOf(task),
|
originalIndex: allTasks.indexOf(task),
|
||||||
@@ -258,20 +373,55 @@ const TaskListV2: React.FC<TaskListV2Props> = ({ projectId }) => {
|
|||||||
return virtuosoGroups.flatMap(group => group.tasks);
|
return virtuosoGroups.flatMap(group => group.tasks);
|
||||||
}, [virtuosoGroups]);
|
}, [virtuosoGroups]);
|
||||||
|
|
||||||
|
// Memoize column headers to prevent unnecessary re-renders
|
||||||
|
const columnHeaders = useMemo(() => (
|
||||||
|
<div className="flex items-center min-w-max px-4 py-2">
|
||||||
|
{visibleColumns.map((column) => {
|
||||||
|
const columnStyle: ColumnStyle = {
|
||||||
|
width: column.width,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={column.id}
|
||||||
|
className="text-xs font-medium text-gray-500 dark:text-gray-400"
|
||||||
|
style={columnStyle}
|
||||||
|
>
|
||||||
|
{column.id === 'dragHandle' ? (
|
||||||
|
<HolderOutlined className="text-gray-400" />
|
||||||
|
) : (
|
||||||
|
column.label
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
), [visibleColumns]);
|
||||||
|
|
||||||
// Render functions
|
// Render functions
|
||||||
const renderGroup = useCallback((groupIndex: number) => {
|
const renderGroup = useCallback((groupIndex: number) => {
|
||||||
const group = virtuosoGroups[groupIndex];
|
const group = virtuosoGroups[groupIndex];
|
||||||
|
const isGroupEmpty = group.count === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TaskGroupHeader
|
<div>
|
||||||
group={{
|
<TaskGroupHeader
|
||||||
id: group.id,
|
group={{
|
||||||
name: group.title,
|
id: group.id,
|
||||||
count: group.count,
|
name: group.title,
|
||||||
color: group.color,
|
count: group.count,
|
||||||
}}
|
color: group.color,
|
||||||
isCollapsed={collapsedGroups.has(group.id)}
|
}}
|
||||||
onToggle={() => handleGroupCollapse(group.id)}
|
isCollapsed={collapsedGroups.has(group.id)}
|
||||||
/>
|
onToggle={() => handleGroupCollapse(group.id)}
|
||||||
|
/>
|
||||||
|
{/* Empty group drop zone */}
|
||||||
|
{isGroupEmpty && !collapsedGroups.has(group.id) && (
|
||||||
|
<div className="px-4 py-8 text-center text-gray-400 dark:text-gray-500 border-2 border-dashed border-transparent hover:border-blue-300 transition-colors">
|
||||||
|
<div className="text-sm">Drop tasks here</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}, [virtuosoGroups, collapsedGroups, handleGroupCollapse]);
|
}, [virtuosoGroups, collapsedGroups, handleGroupCollapse]);
|
||||||
|
|
||||||
@@ -289,19 +439,12 @@ const TaskListV2: React.FC<TaskListV2Props> = ({ projectId }) => {
|
|||||||
if (loading) return <div>Loading...</div>;
|
if (loading) return <div>Loading...</div>;
|
||||||
if (error) return <div>Error: {error}</div>;
|
if (error) return <div>Error: {error}</div>;
|
||||||
|
|
||||||
// Log data for debugging
|
|
||||||
console.log('Rendering with:', {
|
|
||||||
groups,
|
|
||||||
allTasks,
|
|
||||||
virtuosoGroups,
|
|
||||||
virtuosoGroupCounts,
|
|
||||||
virtuosoItems,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col h-screen bg-white dark:bg-gray-900">
|
<div className="flex flex-col h-screen bg-white dark:bg-gray-900">
|
||||||
@@ -313,40 +456,13 @@ const TaskListV2: React.FC<TaskListV2Props> = ({ projectId }) => {
|
|||||||
{/* Column Headers */}
|
{/* Column Headers */}
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<div className="flex-none border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
|
<div className="flex-none border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
|
||||||
<div className="flex items-center min-w-max px-4 py-2">
|
{columnHeaders}
|
||||||
{visibleColumns.map((column, index) => {
|
|
||||||
const columnStyle: ColumnStyle = {
|
|
||||||
width: column.width,
|
|
||||||
// Removed sticky functionality to prevent overlap with group headers
|
|
||||||
// ...(column.isSticky ? {
|
|
||||||
// position: 'sticky',
|
|
||||||
// left: index === 0 ? 0 : index === 1 ? 32 : 132,
|
|
||||||
// backgroundColor: 'inherit',
|
|
||||||
// zIndex: 2,
|
|
||||||
// } : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={column.id}
|
|
||||||
className="text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
||||||
style={columnStyle}
|
|
||||||
>
|
|
||||||
{column.id === 'dragHandle' ? (
|
|
||||||
<HolderOutlined className="text-gray-400" />
|
|
||||||
) : (
|
|
||||||
column.label
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Task List */}
|
{/* Task List */}
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<SortableContext
|
<SortableContext
|
||||||
items={virtuosoItems.map(task => task.id)}
|
items={virtuosoItems.map(task => task.id).filter((id): id is string => id !== undefined)}
|
||||||
strategy={verticalListSortingStrategy}
|
strategy={verticalListSortingStrategy}
|
||||||
>
|
>
|
||||||
<GroupedVirtuoso
|
<GroupedVirtuoso
|
||||||
@@ -372,13 +488,23 @@ const TaskListV2: React.FC<TaskListV2Props> = ({ projectId }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Drag Overlay */}
|
{/* Drag Overlay */}
|
||||||
<DragOverlay>
|
<DragOverlay dropAnimation={null}>
|
||||||
{activeId ? (
|
{activeId ? (
|
||||||
<div className="bg-white dark:bg-gray-800 shadow-lg rounded-md border border-blue-300 opacity-90">
|
<div className="bg-white dark:bg-gray-800 shadow-xl rounded-md border-2 border-blue-400 opacity-95">
|
||||||
<div className="px-4 py-2">
|
<div className="px-4 py-3">
|
||||||
<span className="text-sm font-medium">
|
<div className="flex items-center gap-3">
|
||||||
{allTasks.find(task => task.id === activeId)?.name || 'Task'}
|
<HolderOutlined className="text-blue-500" />
|
||||||
</span>
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{allTasks.find(task => task.id === activeId)?.name ||
|
||||||
|
allTasks.find(task => task.id === activeId)?.title ||
|
||||||
|
'Task'}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{allTasks.find(task => task.id === activeId)?.task_key}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React, { memo, useMemo, useCallback } from 'react';
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { HolderOutlined } from '@ant-design/icons';
|
import { CheckCircleOutlined, HolderOutlined } from '@ant-design/icons';
|
||||||
import { Task } from '@/types/task-management.types';
|
import { Task } from '@/types/task-management.types';
|
||||||
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
|
import { InlineMember } from '@/types/teamMembers/inlineMember.types';
|
||||||
import Avatar from '@/components/Avatar';
|
import Avatar from '@/components/Avatar';
|
||||||
@@ -11,6 +11,8 @@ import { Bars3Icon } from '@heroicons/react/24/outline';
|
|||||||
import { ClockIcon } from '@heroicons/react/24/outline';
|
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 { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
|
|
||||||
interface TaskRowProps {
|
interface TaskRowProps {
|
||||||
task: Task;
|
task: Task;
|
||||||
@@ -30,16 +32,20 @@ const getTaskDisplayName = (task: Task): string => {
|
|||||||
return DEFAULT_TASK_NAME;
|
return DEFAULT_TASK_NAME;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TaskRow: React.FC<TaskRowProps> = ({ task, visibleColumns }) => {
|
// Memoized date formatter to avoid repeated date parsing
|
||||||
|
const formatDate = (dateString: string): string => {
|
||||||
|
try {
|
||||||
|
return format(new Date(dateString), 'MMM d');
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Memoized date formatter to avoid repeated date parsing
|
||||||
|
|
||||||
|
const TaskRow: React.FC<TaskRowProps> = memo(({ task, visibleColumns }) => {
|
||||||
// Drag and drop functionality
|
// Drag and drop functionality
|
||||||
const {
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
setNodeRef,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
isDragging,
|
|
||||||
} = useSortable({
|
|
||||||
id: task.id,
|
id: task.id,
|
||||||
data: {
|
data: {
|
||||||
type: 'task',
|
type: 'task',
|
||||||
@@ -47,52 +53,97 @@ const TaskRow: React.FC<TaskRowProps> = ({ task, visibleColumns }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const style = {
|
// Memoize style object to prevent unnecessary re-renders
|
||||||
|
const style = useMemo(() => ({
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
opacity: isDragging ? 0.5 : 1,
|
opacity: isDragging ? 0.5 : 1,
|
||||||
};
|
}), [transform, transition, isDragging]);
|
||||||
|
|
||||||
// Convert Task to IProjectTask format for AssigneeSelector compatibility
|
// Get dark mode from Redux state
|
||||||
const convertTaskToProjectTask = (task: Task) => {
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
|
const isDarkMode = themeMode === 'dark';
|
||||||
|
|
||||||
|
// Memoize task display name
|
||||||
|
const taskDisplayName = useMemo(() => getTaskDisplayName(task), [task.title, task.name, task.task_key]);
|
||||||
|
|
||||||
|
// Memoize converted task for AssigneeSelector to prevent recreation
|
||||||
|
const convertedTask = useMemo(() => ({
|
||||||
|
id: task.id,
|
||||||
|
name: taskDisplayName,
|
||||||
|
task_key: task.task_key || taskDisplayName,
|
||||||
|
assignees:
|
||||||
|
task.assignee_names?.map((assignee: InlineMember, index: number) => ({
|
||||||
|
team_member_id: assignee.team_member_id || `assignee-${index}`,
|
||||||
|
id: assignee.team_member_id || `assignee-${index}`,
|
||||||
|
project_member_id: assignee.team_member_id || `assignee-${index}`,
|
||||||
|
name: assignee.name || '',
|
||||||
|
})) || [],
|
||||||
|
parent_task_id: task.parent_task_id,
|
||||||
|
status_id: undefined,
|
||||||
|
project_id: undefined,
|
||||||
|
manual_progress: undefined,
|
||||||
|
}), [task.id, taskDisplayName, task.task_key, task.assignee_names, task.parent_task_id]);
|
||||||
|
|
||||||
|
// Memoize formatted dates
|
||||||
|
const formattedDueDate = useMemo(() =>
|
||||||
|
task.dueDate ? formatDate(task.dueDate) : null,
|
||||||
|
[task.dueDate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const formattedStartDate = useMemo(() =>
|
||||||
|
task.startDate ? formatDate(task.startDate) : null,
|
||||||
|
[task.startDate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const formattedCompletedDate = useMemo(() =>
|
||||||
|
task.completedAt ? formatDate(task.completedAt) : null,
|
||||||
|
[task.completedAt]
|
||||||
|
);
|
||||||
|
|
||||||
|
const formattedCreatedDate = useMemo(() =>
|
||||||
|
task.created_at ? formatDate(task.created_at) : null,
|
||||||
|
[task.created_at]
|
||||||
|
);
|
||||||
|
|
||||||
|
const formattedUpdatedDate = useMemo(() =>
|
||||||
|
task.updatedAt ? formatDate(task.updatedAt) : null,
|
||||||
|
[task.updatedAt]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Memoize status style
|
||||||
|
const statusStyle = useMemo(() => ({
|
||||||
|
backgroundColor: task.statusColor ? `${task.statusColor}20` : 'rgb(229, 231, 235)',
|
||||||
|
color: task.statusColor || 'rgb(31, 41, 55)',
|
||||||
|
}), [task.statusColor]);
|
||||||
|
|
||||||
|
// Memoize priority style
|
||||||
|
const priorityStyle = useMemo(() => ({
|
||||||
|
backgroundColor: task.priorityColor ? `${task.priorityColor}20` : 'rgb(229, 231, 235)',
|
||||||
|
color: task.priorityColor || 'rgb(31, 41, 55)',
|
||||||
|
}), [task.priorityColor]);
|
||||||
|
|
||||||
|
// Memoize labels display
|
||||||
|
const labelsDisplay = useMemo(() => {
|
||||||
|
if (!task.labels || task.labels.length === 0) return null;
|
||||||
|
|
||||||
|
const visibleLabels = task.labels.slice(0, 2);
|
||||||
|
const remainingCount = task.labels.length - 2;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: task.id,
|
visibleLabels,
|
||||||
name: getTaskDisplayName(task),
|
remainingCount: remainingCount > 0 ? remainingCount : null,
|
||||||
task_key: task.task_key || getTaskDisplayName(task),
|
|
||||||
assignees:
|
|
||||||
task.assignee_names?.map((assignee: InlineMember, index: number) => ({
|
|
||||||
team_member_id: assignee.team_member_id || `assignee-${index}`,
|
|
||||||
id: assignee.team_member_id || `assignee-${index}`,
|
|
||||||
project_member_id: assignee.team_member_id || `assignee-${index}`,
|
|
||||||
name: assignee.name || '',
|
|
||||||
})) || [],
|
|
||||||
parent_task_id: task.parent_task_id,
|
|
||||||
// Add other required fields with defaults
|
|
||||||
status_id: undefined,
|
|
||||||
project_id: undefined,
|
|
||||||
manual_progress: undefined, // Required field
|
|
||||||
};
|
};
|
||||||
};
|
}, [task.labels]);
|
||||||
|
|
||||||
const renderColumn = (columnId: string, width: string, isSticky?: boolean, index?: number) => {
|
const renderColumn = useCallback((columnId: string, width: string, isSticky?: boolean, index?: number) => {
|
||||||
const baseStyle = {
|
const baseStyle = { width };
|
||||||
width,
|
|
||||||
// Removed sticky functionality to prevent overlap with group headers
|
|
||||||
// ...(isSticky
|
|
||||||
// ? {
|
|
||||||
// position: 'sticky' as const,
|
|
||||||
// left: index === 0 ? 0 : index === 1 ? 32 : 132,
|
|
||||||
// backgroundColor: 'inherit',
|
|
||||||
// zIndex: 1,
|
|
||||||
// }
|
|
||||||
// : {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (columnId) {
|
switch (columnId) {
|
||||||
case 'dragHandle':
|
case 'dragHandle':
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="cursor-grab active:cursor-grabbing flex items-center justify-center"
|
className="cursor-grab active:cursor-grabbing flex items-center justify-center"
|
||||||
style={baseStyle}
|
style={baseStyle}
|
||||||
{...attributes}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
@@ -114,7 +165,7 @@ const TaskRow: React.FC<TaskRowProps> = ({ task, visibleColumns }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center" style={baseStyle}>
|
<div className="flex items-center" style={baseStyle}>
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-300 truncate">
|
<span className="text-sm text-gray-700 dark:text-gray-300 truncate">
|
||||||
{getTaskDisplayName(task)}
|
{taskDisplayName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -124,10 +175,7 @@ const TaskRow: React.FC<TaskRowProps> = ({ task, visibleColumns }) => {
|
|||||||
<div style={baseStyle}>
|
<div style={baseStyle}>
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
|
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
|
||||||
style={{
|
style={statusStyle}
|
||||||
backgroundColor: task.statusColor ? `${task.statusColor}20` : 'rgb(229, 231, 235)',
|
|
||||||
color: task.statusColor || 'rgb(31, 41, 55)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{task.status}
|
{task.status}
|
||||||
</span>
|
</span>
|
||||||
@@ -137,20 +185,16 @@ const TaskRow: React.FC<TaskRowProps> = ({ task, visibleColumns }) => {
|
|||||||
case 'assignees':
|
case 'assignees':
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1" style={baseStyle}>
|
<div className="flex items-center gap-1" style={baseStyle}>
|
||||||
{/* Show existing assignee avatars */}
|
<AvatarGroup
|
||||||
{
|
members={task.assignee_names || []}
|
||||||
<AvatarGroup
|
maxCount={3}
|
||||||
members={task.assignee_names || []}
|
isDarkMode={isDarkMode}
|
||||||
maxCount={3}
|
size={24}
|
||||||
isDarkMode={document.documentElement.classList.contains('dark')}
|
/>
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
{/* Add AssigneeSelector for adding/managing assignees */}
|
|
||||||
<AssigneeSelector
|
<AssigneeSelector
|
||||||
task={convertTaskToProjectTask(task)}
|
task={convertedTask}
|
||||||
groupId={null}
|
groupId={null}
|
||||||
isDarkMode={document.documentElement.classList.contains('dark')}
|
isDarkMode={isDarkMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -160,12 +204,7 @@ const TaskRow: React.FC<TaskRowProps> = ({ task, visibleColumns }) => {
|
|||||||
<div style={baseStyle}>
|
<div style={baseStyle}>
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
|
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
|
||||||
style={{
|
style={priorityStyle}
|
||||||
backgroundColor: task.priorityColor
|
|
||||||
? `${task.priorityColor}20`
|
|
||||||
: 'rgb(229, 231, 235)',
|
|
||||||
color: task.priorityColor || 'rgb(31, 41, 55)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{task.priority}
|
{task.priority}
|
||||||
</span>
|
</span>
|
||||||
@@ -175,9 +214,9 @@ const TaskRow: React.FC<TaskRowProps> = ({ task, visibleColumns }) => {
|
|||||||
case 'dueDate':
|
case 'dueDate':
|
||||||
return (
|
return (
|
||||||
<div style={baseStyle}>
|
<div style={baseStyle}>
|
||||||
{task.dueDate && (
|
{formattedDueDate && (
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{format(new Date(task.dueDate), 'MMM d')}
|
{formattedDueDate}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -186,21 +225,33 @@ const TaskRow: React.FC<TaskRowProps> = ({ task, visibleColumns }) => {
|
|||||||
case 'progress':
|
case 'progress':
|
||||||
return (
|
return (
|
||||||
<div style={baseStyle}>
|
<div style={baseStyle}>
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
|
{task.progress !== undefined &&
|
||||||
<div
|
task.progress >= 0 &&
|
||||||
className="bg-blue-600 h-2 rounded-full"
|
(task.progress === 100 ? (
|
||||||
style={{ width: `${task.progress}%` }}
|
<div className="flex items-center justify-center">
|
||||||
/>
|
<CheckCircleOutlined
|
||||||
</div>
|
className="text-green-500"
|
||||||
|
style={{
|
||||||
|
fontSize: '20px',
|
||||||
|
color: '#52c41a',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<TaskProgress
|
||||||
|
progress={task.progress}
|
||||||
|
numberOfSubTasks={task.sub_tasks?.length || 0}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'labels':
|
case 'labels':
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1" style={baseStyle}>
|
<div className="flex items-center gap-1" style={baseStyle}>
|
||||||
{task.labels?.slice(0, 2).map((label, index) => (
|
{labelsDisplay?.visibleLabels.map((label, index) => (
|
||||||
<span
|
<span
|
||||||
key={index}
|
key={`${label.id}-${index}`}
|
||||||
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
|
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: label.color ? `${label.color}20` : 'rgb(229, 231, 235)',
|
backgroundColor: label.color ? `${label.color}20` : 'rgb(229, 231, 235)',
|
||||||
@@ -210,9 +261,9 @@ const TaskRow: React.FC<TaskRowProps> = ({ task, visibleColumns }) => {
|
|||||||
{label.name}
|
{label.name}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{(task.labels?.length || 0) > 2 && (
|
{labelsDisplay?.remainingCount && (
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
+{task.labels!.length - 2}
|
+{labelsDisplay.remainingCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -256,9 +307,9 @@ const TaskRow: React.FC<TaskRowProps> = ({ task, visibleColumns }) => {
|
|||||||
case 'startDate':
|
case 'startDate':
|
||||||
return (
|
return (
|
||||||
<div style={baseStyle}>
|
<div style={baseStyle}>
|
||||||
{task.startDate && (
|
{formattedStartDate && (
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{format(new Date(task.startDate), 'MMM d')}
|
{formattedStartDate}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -267,9 +318,9 @@ const TaskRow: React.FC<TaskRowProps> = ({ task, visibleColumns }) => {
|
|||||||
case 'completedDate':
|
case 'completedDate':
|
||||||
return (
|
return (
|
||||||
<div style={baseStyle}>
|
<div style={baseStyle}>
|
||||||
{task.completedAt && (
|
{formattedCompletedDate && (
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{format(new Date(task.completedAt), 'MMM d')}
|
{formattedCompletedDate}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -278,9 +329,9 @@ const TaskRow: React.FC<TaskRowProps> = ({ task, visibleColumns }) => {
|
|||||||
case 'createdDate':
|
case 'createdDate':
|
||||||
return (
|
return (
|
||||||
<div style={baseStyle}>
|
<div style={baseStyle}>
|
||||||
{task.created_at && (
|
{formattedCreatedDate && (
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{format(new Date(task.created_at), 'MMM d')}
|
{formattedCreatedDate}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -289,9 +340,9 @@ const TaskRow: React.FC<TaskRowProps> = ({ task, visibleColumns }) => {
|
|||||||
case 'lastUpdated':
|
case 'lastUpdated':
|
||||||
return (
|
return (
|
||||||
<div style={baseStyle}>
|
<div style={baseStyle}>
|
||||||
{task.updatedAt && (
|
{formattedUpdatedDate && (
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{format(new Date(task.updatedAt), 'MMM d')}
|
{formattedUpdatedDate}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -309,10 +360,33 @@ const TaskRow: React.FC<TaskRowProps> = ({ task, visibleColumns }) => {
|
|||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
}, [
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
task.task_key,
|
||||||
|
task.status,
|
||||||
|
task.priority,
|
||||||
|
task.phase,
|
||||||
|
task.reporter,
|
||||||
|
task.assignee_names,
|
||||||
|
task.timeTracking,
|
||||||
|
task.progress,
|
||||||
|
task.sub_tasks,
|
||||||
|
taskDisplayName,
|
||||||
|
statusStyle,
|
||||||
|
priorityStyle,
|
||||||
|
formattedDueDate,
|
||||||
|
formattedStartDate,
|
||||||
|
formattedCompletedDate,
|
||||||
|
formattedCreatedDate,
|
||||||
|
formattedUpdatedDate,
|
||||||
|
labelsDisplay,
|
||||||
|
isDarkMode,
|
||||||
|
convertedTask,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
style={style}
|
style={style}
|
||||||
className={`flex items-center min-w-max px-4 py-2 hover:bg-gray-50 dark:hover:bg-gray-800 ${
|
className={`flex items-center min-w-max px-4 py-2 hover:bg-gray-50 dark:hover:bg-gray-800 ${
|
||||||
@@ -324,6 +398,8 @@ const TaskRow: React.FC<TaskRowProps> = ({ task, visibleColumns }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
TaskRow.displayName = 'TaskRow';
|
||||||
|
|
||||||
export default TaskRow;
|
export default TaskRow;
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ export const fetchTasksV3 = createAsyncThunk(
|
|||||||
// Log raw response for debugging
|
// Log raw response for debugging
|
||||||
console.log('Raw API response:', response.body);
|
console.log('Raw API response:', response.body);
|
||||||
console.log('Sample task from backend:', response.body.allTasks?.[0]);
|
console.log('Sample task from backend:', response.body.allTasks?.[0]);
|
||||||
|
console.log('Task key from backend:', response.body.allTasks?.[0]?.task_key);
|
||||||
|
|
||||||
// Ensure tasks are properly normalized
|
// Ensure tasks are properly normalized
|
||||||
const tasks = response.body.allTasks.map((task: any) => {
|
const tasks = response.body.allTasks.map((task: any) => {
|
||||||
@@ -232,6 +233,7 @@ export const fetchTasksV3 = createAsyncThunk(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: task.id,
|
id: task.id,
|
||||||
|
task_key: task.task_key || task.key || '',
|
||||||
title: (task.title && task.title.trim()) ? task.title.trim() : DEFAULT_TASK_NAME,
|
title: (task.title && task.title.trim()) ? task.title.trim() : DEFAULT_TASK_NAME,
|
||||||
description: task.description || '',
|
description: task.description || '',
|
||||||
status: task.status || 'todo',
|
status: task.status || 'todo',
|
||||||
@@ -327,7 +329,7 @@ export const fetchSubTasks = createAsyncThunk(
|
|||||||
const config: ITaskListConfigV2 = {
|
const config: ITaskListConfigV2 = {
|
||||||
id: projectId,
|
id: projectId,
|
||||||
archived: false,
|
archived: false,
|
||||||
group: currentGrouping,
|
group: currentGrouping || '',
|
||||||
field: '',
|
field: '',
|
||||||
order: '',
|
order: '',
|
||||||
search: '',
|
search: '',
|
||||||
@@ -644,7 +646,7 @@ const taskManagementSlice = createSlice({
|
|||||||
} else {
|
} else {
|
||||||
// Set empty state but don't show error
|
// Set empty state but don't show error
|
||||||
state.ids = [];
|
state.ids = [];
|
||||||
state.entities = {};
|
state.entities = {} as Record<string, Task>;
|
||||||
state.groups = [];
|
state.groups = [];
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -654,7 +656,7 @@ const taskManagementSlice = createSlice({
|
|||||||
state.error = action.error.message || action.payload || 'An error occurred while fetching tasks. Please try again.';
|
state.error = action.error.message || action.payload || 'An error occurred while fetching tasks. Please try again.';
|
||||||
// Clear task data on error to prevent stale state
|
// Clear task data on error to prevent stale state
|
||||||
state.ids = [];
|
state.ids = [];
|
||||||
state.entities = {};
|
state.entities = {} as Record<string, Task>;
|
||||||
state.groups = [];
|
state.groups = [];
|
||||||
})
|
})
|
||||||
.addCase(fetchSubTasks.pending, (state, action) => {
|
.addCase(fetchSubTasks.pending, (state, action) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user