Merge pull request #194 from Worklenz/fix/task-list-realtime-update
Fix/task list realtime update
This commit is contained in:
@@ -308,7 +308,6 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(({
|
|||||||
<Text type="secondary">No tasks in this group</Text>
|
<Text type="secondary">No tasks in this group</Text>
|
||||||
<br />
|
<br />
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
|
||||||
{...taskManagementAntdConfig.taskButtonDefaults}
|
{...taskManagementAntdConfig.taskButtonDefaults}
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={handleAddTask}
|
onClick={handleAddTask}
|
||||||
@@ -599,10 +598,11 @@ const TaskGroup: React.FC<TaskGroupProps> = React.memo(({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, (prevProps, nextProps) => {
|
}, (prevProps, nextProps) => {
|
||||||
// Simplified comparison for better performance
|
// More comprehensive comparison to detect task movements
|
||||||
return (
|
return (
|
||||||
prevProps.group.id === nextProps.group.id &&
|
prevProps.group.id === nextProps.group.id &&
|
||||||
prevProps.group.taskIds.length === nextProps.group.taskIds.length &&
|
prevProps.group.taskIds.length === nextProps.group.taskIds.length &&
|
||||||
|
prevProps.group.taskIds.every((id, index) => id === nextProps.group.taskIds[index]) &&
|
||||||
prevProps.group.collapsed === nextProps.group.collapsed &&
|
prevProps.group.collapsed === nextProps.group.collapsed &&
|
||||||
prevProps.selectedTaskIds.length === nextProps.selectedTaskIds.length &&
|
prevProps.selectedTaskIds.length === nextProps.selectedTaskIds.length &&
|
||||||
prevProps.currentGrouping === nextProps.currentGrouping
|
prevProps.currentGrouping === nextProps.currentGrouping
|
||||||
|
|||||||
@@ -0,0 +1,298 @@
|
|||||||
|
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
|
import { useSocket } from '@/socket/socketContext';
|
||||||
|
import { SocketEvents } from '@/shared/socket-events';
|
||||||
|
import { Task } from '@/types/task-management.types';
|
||||||
|
import { ClearOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
interface TaskPhaseDropdownProps {
|
||||||
|
task: Task;
|
||||||
|
projectId: string;
|
||||||
|
isDarkMode?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TaskPhaseDropdown: React.FC<TaskPhaseDropdownProps> = ({
|
||||||
|
task,
|
||||||
|
projectId,
|
||||||
|
isDarkMode = false
|
||||||
|
}) => {
|
||||||
|
const { socket, connected } = useSocket();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const { phaseList } = useAppSelector(state => state.phaseReducer);
|
||||||
|
|
||||||
|
// Find current phase details
|
||||||
|
const currentPhase = useMemo(() => {
|
||||||
|
return phaseList.find(phase => phase.name === task.phase);
|
||||||
|
}, [phaseList, task.phase]);
|
||||||
|
|
||||||
|
// Handle phase change
|
||||||
|
const handlePhaseChange = useCallback((phaseId: string, phaseName: string) => {
|
||||||
|
if (!task.id || !phaseId || !connected) return;
|
||||||
|
|
||||||
|
console.log('🎯 Phase change initiated:', { taskId: task.id, phaseId, phaseName });
|
||||||
|
|
||||||
|
socket?.emit(
|
||||||
|
SocketEvents.TASK_PHASE_CHANGE.toString(),
|
||||||
|
{
|
||||||
|
task_id: task.id,
|
||||||
|
phase_id: phaseId,
|
||||||
|
parent_task: null, // Assuming top-level tasks for now
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setIsOpen(false);
|
||||||
|
}, [task.id, connected, socket]);
|
||||||
|
|
||||||
|
// Handle phase clear
|
||||||
|
const handlePhaseClear = useCallback(() => {
|
||||||
|
if (!task.id || !connected) return;
|
||||||
|
|
||||||
|
console.log('🎯 Phase clear initiated:', { taskId: task.id });
|
||||||
|
|
||||||
|
socket?.emit(
|
||||||
|
SocketEvents.TASK_PHASE_CHANGE.toString(),
|
||||||
|
{
|
||||||
|
task_id: task.id,
|
||||||
|
phase_id: null,
|
||||||
|
parent_task: null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setIsOpen(false);
|
||||||
|
}, [task.id, connected, socket]);
|
||||||
|
|
||||||
|
// Calculate dropdown position and handle outside clicks
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (buttonRef.current && buttonRef.current.contains(event.target as Node)) {
|
||||||
|
return; // Don't close if clicking the button
|
||||||
|
}
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isOpen && buttonRef.current) {
|
||||||
|
// Calculate position
|
||||||
|
const rect = buttonRef.current.getBoundingClientRect();
|
||||||
|
setDropdownPosition({
|
||||||
|
top: rect.bottom + window.scrollY + 4,
|
||||||
|
left: rect.left + window.scrollX,
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Get phase color
|
||||||
|
const getPhaseColor = useCallback((phase: any) => {
|
||||||
|
return phase?.color_code || '#722ed1';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Format phase name for display
|
||||||
|
const formatPhaseName = useCallback((name: string) => {
|
||||||
|
if (!name) return 'Select';
|
||||||
|
return name;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Determine if no phase is selected
|
||||||
|
const hasPhase = task.phase && task.phase.trim() !== '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Phase Button - Show "Select" when no phase */}
|
||||||
|
<button
|
||||||
|
ref={buttonRef}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
}}
|
||||||
|
className={`
|
||||||
|
inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium
|
||||||
|
transition-all duration-200 hover:opacity-80 border-0 min-w-[70px] max-w-full justify-center
|
||||||
|
whitespace-nowrap
|
||||||
|
`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: hasPhase && currentPhase
|
||||||
|
? getPhaseColor(currentPhase)
|
||||||
|
: (isDarkMode ? '#4b5563' : '#9ca3af'),
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="truncate">
|
||||||
|
{hasPhase && currentPhase ? formatPhaseName(currentPhase.name || '') : 'Select'}
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
className={`w-3 h-3 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Dropdown Menu */}
|
||||||
|
{isOpen && createPortal(
|
||||||
|
<div
|
||||||
|
ref={dropdownRef}
|
||||||
|
className={`
|
||||||
|
fixed min-w-[160px] max-w-[220px]
|
||||||
|
rounded border backdrop-blur-sm z-[9999]
|
||||||
|
${isDarkMode
|
||||||
|
? 'bg-gray-900/95 border-gray-600 shadow-2xl shadow-black/50'
|
||||||
|
: 'bg-white/95 border-gray-200 shadow-2xl shadow-gray-500/20'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
style={{
|
||||||
|
top: dropdownPosition.top,
|
||||||
|
left: dropdownPosition.left,
|
||||||
|
zIndex: 9999,
|
||||||
|
animation: 'fadeInScale 0.15s ease-out',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Phase Options */}
|
||||||
|
<div className="py-1 max-h-64 overflow-y-auto">
|
||||||
|
{/* No Phase Option */}
|
||||||
|
<button
|
||||||
|
onClick={handlePhaseClear}
|
||||||
|
className={`
|
||||||
|
w-full px-3 py-2.5 text-left text-xs font-medium flex items-center gap-3
|
||||||
|
transition-all duration-150 hover:scale-[1.02] active:scale-[0.98]
|
||||||
|
${isDarkMode
|
||||||
|
? 'hover:bg-gray-700/80 text-gray-100'
|
||||||
|
: 'hover:bg-gray-50/70 text-gray-900'
|
||||||
|
}
|
||||||
|
${!hasPhase
|
||||||
|
? (isDarkMode ? 'bg-gray-700/60 ring-1 ring-blue-400/40' : 'bg-blue-50/50 ring-1 ring-blue-200')
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
style={{
|
||||||
|
animation: 'slideInFromLeft 0.2s ease-out forwards',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Clear Icon */}
|
||||||
|
<div className="flex items-center justify-center w-4 h-4">
|
||||||
|
<ClearOutlined className="w-3 h-3" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* No Phase Color Indicator */}
|
||||||
|
<div
|
||||||
|
className={`w-3 h-3 rounded-full shadow-sm border-2 ${
|
||||||
|
isDarkMode ? 'border-gray-800/30' : 'border-white/20'
|
||||||
|
}`}
|
||||||
|
style={{ backgroundColor: isDarkMode ? '#4b5563' : '#9ca3af' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* No Phase Text */}
|
||||||
|
<span className="flex-1 truncate">No Phase</span>
|
||||||
|
|
||||||
|
{/* Current Selection Badge */}
|
||||||
|
{!hasPhase && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className={`w-1.5 h-1.5 rounded-full ${isDarkMode ? 'bg-blue-400' : 'bg-blue-500'}`} />
|
||||||
|
<span className={`text-xs font-medium ${isDarkMode ? 'text-blue-300' : 'text-blue-600'}`}>
|
||||||
|
Current
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Phase Options */}
|
||||||
|
{phaseList.map((phase, index) => {
|
||||||
|
const isSelected = phase.name === task.phase;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={phase.id}
|
||||||
|
onClick={() => handlePhaseChange(phase.id!, phase.name!)}
|
||||||
|
className={`
|
||||||
|
w-full px-3 py-2.5 text-left text-xs font-medium flex items-center gap-3
|
||||||
|
transition-all duration-150 hover:scale-[1.02] active:scale-[0.98]
|
||||||
|
${isDarkMode
|
||||||
|
? 'hover:bg-gray-700/80 text-gray-100'
|
||||||
|
: 'hover:bg-gray-50/70 text-gray-900'
|
||||||
|
}
|
||||||
|
${isSelected
|
||||||
|
? (isDarkMode ? 'bg-gray-700/60 ring-1 ring-blue-400/40' : 'bg-blue-50/50 ring-1 ring-blue-200')
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
style={{
|
||||||
|
animationDelay: `${(index + 1) * 30}ms`,
|
||||||
|
animation: 'slideInFromLeft 0.2s ease-out forwards',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Phase Color Indicator */}
|
||||||
|
<div
|
||||||
|
className={`w-3 h-3 rounded-full shadow-sm border-2 ${
|
||||||
|
isDarkMode ? 'border-gray-800/30' : 'border-white/20'
|
||||||
|
}`}
|
||||||
|
style={{ backgroundColor: getPhaseColor(phase) }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Phase Name */}
|
||||||
|
<span className="flex-1 truncate">
|
||||||
|
{formatPhaseName(phase.name || '')}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Current Phase Badge */}
|
||||||
|
{isSelected && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className={`w-1.5 h-1.5 rounded-full ${isDarkMode ? 'bg-blue-400' : 'bg-blue-500'}`} />
|
||||||
|
<span className={`text-xs font-medium ${isDarkMode ? 'text-blue-300' : 'text-blue-600'}`}>
|
||||||
|
Current
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* CSS Animations */}
|
||||||
|
{isOpen && createPortal(
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
@keyframes fadeInScale {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(-5px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInFromLeft {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>,
|
||||||
|
document.head
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskPhaseDropdown;
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
|
import { useSocket } from '@/socket/socketContext';
|
||||||
|
import { SocketEvents } from '@/shared/socket-events';
|
||||||
|
import { Task } from '@/types/task-management.types';
|
||||||
|
import { MinusOutlined, PauseOutlined, DoubleRightOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
interface TaskPriorityDropdownProps {
|
||||||
|
task: Task;
|
||||||
|
projectId: string;
|
||||||
|
isDarkMode?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TaskPriorityDropdown: React.FC<TaskPriorityDropdownProps> = ({
|
||||||
|
task,
|
||||||
|
projectId,
|
||||||
|
isDarkMode = false
|
||||||
|
}) => {
|
||||||
|
const { socket, connected } = useSocket();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const priorityList = useAppSelector(state => state.priorityReducer.priorities);
|
||||||
|
|
||||||
|
// Find current priority details
|
||||||
|
const currentPriority = useMemo(() => {
|
||||||
|
return priorityList.find(priority =>
|
||||||
|
priority.name?.toLowerCase() === task.priority?.toLowerCase() ||
|
||||||
|
priority.id === task.priority
|
||||||
|
);
|
||||||
|
}, [priorityList, task.priority]);
|
||||||
|
|
||||||
|
// Handle priority change
|
||||||
|
const handlePriorityChange = useCallback((priorityId: string, priorityName: string) => {
|
||||||
|
if (!task.id || !priorityId || !connected) return;
|
||||||
|
|
||||||
|
console.log('🎯 Priority change initiated:', { taskId: task.id, priorityId, priorityName });
|
||||||
|
|
||||||
|
socket?.emit(
|
||||||
|
SocketEvents.TASK_PRIORITY_CHANGE.toString(),
|
||||||
|
JSON.stringify({
|
||||||
|
task_id: task.id,
|
||||||
|
priority_id: priorityId,
|
||||||
|
team_id: projectId, // Using projectId as teamId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setIsOpen(false);
|
||||||
|
}, [task.id, connected, socket, projectId]);
|
||||||
|
|
||||||
|
// Calculate dropdown position and handle outside clicks
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (buttonRef.current && buttonRef.current.contains(event.target as Node)) {
|
||||||
|
return; // Don't close if clicking the button
|
||||||
|
}
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isOpen && buttonRef.current) {
|
||||||
|
// Calculate position
|
||||||
|
const rect = buttonRef.current.getBoundingClientRect();
|
||||||
|
setDropdownPosition({
|
||||||
|
top: rect.bottom + window.scrollY + 4,
|
||||||
|
left: rect.left + window.scrollX,
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Get priority color
|
||||||
|
const getPriorityColor = useCallback((priority: any) => {
|
||||||
|
if (isDarkMode) {
|
||||||
|
return priority?.color_code_dark || priority?.color_code || '#4b5563';
|
||||||
|
}
|
||||||
|
return priority?.color_code || '#6b7280';
|
||||||
|
}, [isDarkMode]);
|
||||||
|
|
||||||
|
// Get priority icon
|
||||||
|
const getPriorityIcon = useCallback((priorityName: string) => {
|
||||||
|
const name = priorityName?.toLowerCase();
|
||||||
|
switch (name) {
|
||||||
|
case 'low':
|
||||||
|
return <MinusOutlined className="w-3 h-3" />;
|
||||||
|
case 'medium':
|
||||||
|
return <PauseOutlined className="w-3 h-3" style={{ transform: 'rotate(90deg)' }} />;
|
||||||
|
case 'high':
|
||||||
|
return <DoubleRightOutlined className="w-3 h-3" style={{ transform: 'rotate(90deg)' }} />;
|
||||||
|
default:
|
||||||
|
return <MinusOutlined className="w-3 h-3" />;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Format priority name for display
|
||||||
|
const formatPriorityName = useCallback((name: string) => {
|
||||||
|
if (!name) return name;
|
||||||
|
return name.charAt(0).toUpperCase() + name.slice(1).toLowerCase();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!task.priority) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Priority Button - Simple text display like status */}
|
||||||
|
<button
|
||||||
|
ref={buttonRef}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
}}
|
||||||
|
className={`
|
||||||
|
inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium
|
||||||
|
transition-all duration-200 hover:opacity-80 border-0 min-w-[70px] max-w-full justify-center
|
||||||
|
whitespace-nowrap
|
||||||
|
`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: currentPriority ? getPriorityColor(currentPriority) : (isDarkMode ? '#4b5563' : '#9ca3af'),
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="truncate">
|
||||||
|
{currentPriority ? formatPriorityName(currentPriority.name || '') : formatPriorityName(task.priority)}
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
className={`w-3 h-3 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Dropdown Menu */}
|
||||||
|
{isOpen && createPortal(
|
||||||
|
<div
|
||||||
|
ref={dropdownRef}
|
||||||
|
className={`
|
||||||
|
fixed min-w-[160px] max-w-[220px]
|
||||||
|
rounded border backdrop-blur-sm z-[9999]
|
||||||
|
${isDarkMode
|
||||||
|
? 'bg-gray-900/95 border-gray-600 shadow-2xl shadow-black/50'
|
||||||
|
: 'bg-white/95 border-gray-200 shadow-2xl shadow-gray-500/20'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
style={{
|
||||||
|
top: dropdownPosition.top,
|
||||||
|
left: dropdownPosition.left,
|
||||||
|
zIndex: 9999,
|
||||||
|
animation: 'fadeInScale 0.15s ease-out',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Priority Options */}
|
||||||
|
<div className="py-1 max-h-64 overflow-y-auto">
|
||||||
|
{priorityList.map((priority, index) => {
|
||||||
|
const isSelected = priority.name?.toLowerCase() === task.priority?.toLowerCase() || priority.id === task.priority;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={priority.id}
|
||||||
|
onClick={() => handlePriorityChange(priority.id!, priority.name!)}
|
||||||
|
className={`
|
||||||
|
w-full px-3 py-2.5 text-left text-xs font-medium flex items-center gap-3
|
||||||
|
transition-all duration-150 hover:scale-[1.02] active:scale-[0.98]
|
||||||
|
${isDarkMode
|
||||||
|
? 'hover:bg-gray-700/80 text-gray-100'
|
||||||
|
: 'hover:bg-gray-50/70 text-gray-900'
|
||||||
|
}
|
||||||
|
${isSelected
|
||||||
|
? (isDarkMode ? 'bg-gray-700/60 ring-1 ring-blue-400/40' : 'bg-blue-50/50 ring-1 ring-blue-200')
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
style={{
|
||||||
|
animationDelay: `${index * 30}ms`,
|
||||||
|
animation: 'slideInFromLeft 0.2s ease-out forwards',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Priority Icon */}
|
||||||
|
<div className="flex items-center justify-center w-4 h-4">
|
||||||
|
{getPriorityIcon(priority.name || '')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Priority Color Indicator */}
|
||||||
|
<div
|
||||||
|
className={`w-3 h-3 rounded-full shadow-sm border-2 ${
|
||||||
|
isDarkMode ? 'border-gray-800/30' : 'border-white/20'
|
||||||
|
}`}
|
||||||
|
style={{ backgroundColor: getPriorityColor(priority) }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Priority Name */}
|
||||||
|
<span className="flex-1 truncate">
|
||||||
|
{formatPriorityName(priority.name || '')}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Current Priority Badge */}
|
||||||
|
{isSelected && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className={`w-1.5 h-1.5 rounded-full ${isDarkMode ? 'bg-blue-400' : 'bg-blue-500'}`} />
|
||||||
|
<span className={`text-xs font-medium ${isDarkMode ? 'text-blue-300' : 'text-blue-600'}`}>
|
||||||
|
Current
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* CSS Animations */}
|
||||||
|
{isOpen && createPortal(
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
@keyframes fadeInScale {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95) translateY(-5px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInFromLeft {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>,
|
||||||
|
document.head
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskPriorityDropdown;
|
||||||
@@ -22,6 +22,8 @@ import { AssigneeSelector, Avatar, AvatarGroup, Button, Checkbox, CustomColordLa
|
|||||||
import { useSocket } from '@/socket/socketContext';
|
import { useSocket } from '@/socket/socketContext';
|
||||||
import { SocketEvents } from '@/shared/socket-events';
|
import { SocketEvents } from '@/shared/socket-events';
|
||||||
import TaskStatusDropdown from './task-status-dropdown';
|
import TaskStatusDropdown from './task-status-dropdown';
|
||||||
|
import TaskPriorityDropdown from './task-priority-dropdown';
|
||||||
|
import TaskPhaseDropdown from './task-phase-dropdown';
|
||||||
import {
|
import {
|
||||||
formatDate as utilFormatDate,
|
formatDate as utilFormatDate,
|
||||||
formatDateTime as utilFormatDateTime,
|
formatDateTime as utilFormatDateTime,
|
||||||
@@ -419,6 +421,24 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case 'priority':
|
||||||
|
return (
|
||||||
|
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||||
|
<div className={`px-2 py-1 text-xs rounded ${isDarkMode ? 'bg-gray-700 text-gray-300' : 'bg-gray-100 text-gray-600'}`}>
|
||||||
|
{task.priority || 'Medium'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'phase':
|
||||||
|
return (
|
||||||
|
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
||||||
|
<div className={`px-2 py-1 text-xs rounded ${isDarkMode ? 'bg-gray-700 text-gray-300' : 'bg-gray-100 text-gray-600'}`}>
|
||||||
|
{task.phase || 'No Phase'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// For non-essential columns, show placeholder during initial load
|
// For non-essential columns, show placeholder during initial load
|
||||||
return (
|
return (
|
||||||
@@ -580,10 +600,14 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
|
|
||||||
case 'phase':
|
case 'phase':
|
||||||
return (
|
return (
|
||||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
<div key={col.key} className={`flex items-center px-2 ${borderClasses} overflow-visible`} style={{ width: col.width, minWidth: col.width }}>
|
||||||
<span className={`text-xs ${isDarkMode ? 'text-gray-300' : 'text-gray-700'}`}>
|
<div className="w-full">
|
||||||
{task.phase || 'No Phase'}
|
<TaskPhaseDropdown
|
||||||
</span>
|
task={task}
|
||||||
|
projectId={projectId}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -602,8 +626,14 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
|
|
||||||
case 'priority':
|
case 'priority':
|
||||||
return (
|
return (
|
||||||
<div key={col.key} className={`flex items-center px-2 ${borderClasses}`} style={{ width: col.width }}>
|
<div key={col.key} className={`flex items-center px-2 ${borderClasses} overflow-visible`} style={{ width: col.width, minWidth: col.width }}>
|
||||||
<TaskPriority priority={task.priority} isDarkMode={isDarkMode} />
|
<div className="w-full">
|
||||||
|
<TaskPriorityDropdown
|
||||||
|
task={task}
|
||||||
|
projectId={projectId}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -226,6 +226,37 @@ const taskManagementSlice = createSlice({
|
|||||||
tasksAdapter.addOne(state, action.payload);
|
tasksAdapter.addOne(state, action.payload);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
addTaskToGroup: (state, action: PayloadAction<{ task: Task; groupId?: string }>) => {
|
||||||
|
const { task, groupId } = action.payload;
|
||||||
|
|
||||||
|
// Add to entity adapter
|
||||||
|
tasksAdapter.addOne(state, task);
|
||||||
|
|
||||||
|
// Add to groups array for V3 API compatibility
|
||||||
|
if (state.groups && state.groups.length > 0) {
|
||||||
|
// Find the target group using the provided UUID
|
||||||
|
const targetGroup = state.groups.find(group => {
|
||||||
|
// If a specific groupId (UUID) is provided, use it directly
|
||||||
|
if (groupId && group.id === groupId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (targetGroup) {
|
||||||
|
// Add task ID to the end of the group's taskIds array (newest last)
|
||||||
|
targetGroup.taskIds.push(task.id);
|
||||||
|
|
||||||
|
// Also add to the tasks array if it exists (for backward compatibility)
|
||||||
|
if ((targetGroup as any).tasks) {
|
||||||
|
(targetGroup as any).tasks.push(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
updateTask: (state, action: PayloadAction<{ id: string; changes: Partial<Task> }>) => {
|
updateTask: (state, action: PayloadAction<{ id: string; changes: Partial<Task> }>) => {
|
||||||
tasksAdapter.updateOne(state, {
|
tasksAdapter.updateOne(state, {
|
||||||
id: action.payload.id,
|
id: action.payload.id,
|
||||||
@@ -290,6 +321,60 @@ const taskManagementSlice = createSlice({
|
|||||||
|
|
||||||
tasksAdapter.updateOne(state, { id: taskId, changes });
|
tasksAdapter.updateOne(state, { id: taskId, changes });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// New action to move task between groups with proper group management
|
||||||
|
moveTaskBetweenGroups: (state, action: PayloadAction<{
|
||||||
|
taskId: string;
|
||||||
|
fromGroupId: string;
|
||||||
|
toGroupId: string;
|
||||||
|
taskUpdate: Partial<Task>;
|
||||||
|
}>) => {
|
||||||
|
const { taskId, fromGroupId, toGroupId, taskUpdate } = action.payload;
|
||||||
|
|
||||||
|
console.log('🔧 moveTaskBetweenGroups action:', {
|
||||||
|
taskId,
|
||||||
|
fromGroupId,
|
||||||
|
toGroupId,
|
||||||
|
taskUpdate,
|
||||||
|
hasGroups: !!state.groups,
|
||||||
|
groupsCount: state.groups?.length || 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the task entity with new values
|
||||||
|
tasksAdapter.updateOne(state, {
|
||||||
|
id: taskId,
|
||||||
|
changes: {
|
||||||
|
...taskUpdate,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update groups if they exist
|
||||||
|
if (state.groups && state.groups.length > 0) {
|
||||||
|
// Remove task from old group
|
||||||
|
const fromGroup = state.groups.find(group => group.id === fromGroupId);
|
||||||
|
if (fromGroup) {
|
||||||
|
const beforeCount = fromGroup.taskIds.length;
|
||||||
|
fromGroup.taskIds = fromGroup.taskIds.filter(id => id !== taskId);
|
||||||
|
console.log(`🔧 Removed task from ${fromGroup.title}: ${beforeCount} -> ${fromGroup.taskIds.length}`);
|
||||||
|
} else {
|
||||||
|
console.warn('🚨 From group not found:', fromGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add task to new group
|
||||||
|
const toGroup = state.groups.find(group => group.id === toGroupId);
|
||||||
|
if (toGroup) {
|
||||||
|
const beforeCount = toGroup.taskIds.length;
|
||||||
|
// Add to the end of the group (newest last)
|
||||||
|
toGroup.taskIds.push(taskId);
|
||||||
|
console.log(`🔧 Added task to ${toGroup.title}: ${beforeCount} -> ${toGroup.taskIds.length}`);
|
||||||
|
} else {
|
||||||
|
console.warn('🚨 To group not found:', toGroupId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('🚨 No groups available for task movement');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Optimistic update for drag operations - reduces perceived lag
|
// Optimistic update for drag operations - reduces perceived lag
|
||||||
optimisticTaskMove: (state, action: PayloadAction<{ taskId: string; newGroupId: string; newIndex: number }>) => {
|
optimisticTaskMove: (state, action: PayloadAction<{ taskId: string; newGroupId: string; newIndex: number }>) => {
|
||||||
@@ -392,12 +477,14 @@ const taskManagementSlice = createSlice({
|
|||||||
export const {
|
export const {
|
||||||
setTasks,
|
setTasks,
|
||||||
addTask,
|
addTask,
|
||||||
|
addTaskToGroup,
|
||||||
updateTask,
|
updateTask,
|
||||||
deleteTask,
|
deleteTask,
|
||||||
bulkUpdateTasks,
|
bulkUpdateTasks,
|
||||||
bulkDeleteTasks,
|
bulkDeleteTasks,
|
||||||
reorderTasks,
|
reorderTasks,
|
||||||
moveTaskToGroup,
|
moveTaskToGroup,
|
||||||
|
moveTaskBetweenGroups,
|
||||||
optimisticTaskMove,
|
optimisticTaskMove,
|
||||||
setLoading,
|
setLoading,
|
||||||
setError,
|
setError,
|
||||||
|
|||||||
@@ -33,10 +33,12 @@ import {
|
|||||||
updateSubTasks,
|
updateSubTasks,
|
||||||
updateTaskProgress,
|
updateTaskProgress,
|
||||||
} from '@/features/tasks/tasks.slice';
|
} from '@/features/tasks/tasks.slice';
|
||||||
import {
|
import {
|
||||||
addTask,
|
addTask,
|
||||||
|
addTaskToGroup,
|
||||||
updateTask,
|
updateTask,
|
||||||
moveTaskToGroup,
|
moveTaskToGroup,
|
||||||
|
moveTaskBetweenGroups,
|
||||||
selectCurrentGroupingV3,
|
selectCurrentGroupingV3,
|
||||||
fetchTasksV3
|
fetchTasksV3
|
||||||
} from '@/features/task-management/task-management.slice';
|
} from '@/features/task-management/task-management.slice';
|
||||||
@@ -136,14 +138,66 @@ export const useTaskSocketHandlers = () => {
|
|||||||
dispatch(updateTaskStatus(response));
|
dispatch(updateTaskStatus(response));
|
||||||
dispatch(deselectAll());
|
dispatch(deselectAll());
|
||||||
|
|
||||||
// For the task management slice, let's use a simpler approach:
|
// For the task management slice, move task between groups without resetting
|
||||||
// Just refetch the tasks to ensure consistency
|
const state = store.getState();
|
||||||
if (response.id && projectId) {
|
const groups = state.taskManagement.groups;
|
||||||
console.log('🔄 Refetching tasks after status change to ensure consistency...');
|
const currentTask = state.taskManagement.entities[response.id];
|
||||||
dispatch(fetchTasksV3(projectId));
|
|
||||||
|
console.log('🔍 Status change debug:', {
|
||||||
|
hasGroups: !!groups,
|
||||||
|
groupsLength: groups?.length || 0,
|
||||||
|
hasCurrentTask: !!currentTask,
|
||||||
|
statusId: response.status_id,
|
||||||
|
currentGrouping: state.taskManagement.grouping
|
||||||
|
});
|
||||||
|
|
||||||
|
if (groups && groups.length > 0 && currentTask && response.status_id) {
|
||||||
|
// Find current group containing the task
|
||||||
|
const currentGroup = groups.find(group => group.taskIds.includes(response.id));
|
||||||
|
|
||||||
|
// Find target group based on new status ID
|
||||||
|
// The status_id from response is the UUID of the new status
|
||||||
|
const targetGroup = groups.find(group => group.id === response.status_id);
|
||||||
|
|
||||||
|
if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) {
|
||||||
|
console.log('🔄 Moving task between groups:', {
|
||||||
|
taskId: response.id,
|
||||||
|
fromGroup: currentGroup.title,
|
||||||
|
toGroup: targetGroup.title
|
||||||
|
});
|
||||||
|
|
||||||
|
// Determine the new status value based on status category
|
||||||
|
let newStatusValue: 'todo' | 'doing' | 'done' = 'todo';
|
||||||
|
if (response.statusCategory) {
|
||||||
|
if (response.statusCategory.is_done) {
|
||||||
|
newStatusValue = 'done';
|
||||||
|
} else if (response.statusCategory.is_doing) {
|
||||||
|
newStatusValue = 'doing';
|
||||||
|
} else {
|
||||||
|
newStatusValue = 'todo';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the new action to move task between groups
|
||||||
|
dispatch(moveTaskBetweenGroups({
|
||||||
|
taskId: response.id,
|
||||||
|
fromGroupId: currentGroup.id,
|
||||||
|
toGroupId: targetGroup.id,
|
||||||
|
taskUpdate: {
|
||||||
|
status: newStatusValue,
|
||||||
|
progress: response.complete_ratio || currentTask.progress,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} else if (!currentGroup || !targetGroup) {
|
||||||
|
// Fallback to refetch if groups not found (shouldn't happen normally)
|
||||||
|
console.log('🔄 Groups not found, refetching tasks...');
|
||||||
|
if (projectId) {
|
||||||
|
dispatch(fetchTasksV3(projectId));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch, currentGroupingV3]
|
[dispatch, currentGroupingV3, projectId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTaskProgress = useCallback(
|
const handleTaskProgress = useCallback(
|
||||||
@@ -186,15 +240,82 @@ export const useTaskSocketHandlers = () => {
|
|||||||
(response: ITaskListPriorityChangeResponse) => {
|
(response: ITaskListPriorityChangeResponse) => {
|
||||||
if (!response) return;
|
if (!response) return;
|
||||||
|
|
||||||
|
console.log('🎯 Priority change received:', response);
|
||||||
|
|
||||||
// Update the old task slice (for backward compatibility)
|
// Update the old task slice (for backward compatibility)
|
||||||
dispatch(updateTaskPriority(response));
|
dispatch(updateTaskPriority(response));
|
||||||
dispatch(setTaskPriority(response));
|
dispatch(setTaskPriority(response));
|
||||||
dispatch(deselectAll());
|
dispatch(deselectAll());
|
||||||
|
|
||||||
// For the task management slice, refetch tasks to ensure consistency
|
// For the task management slice, always update the task entity first
|
||||||
if (response.id && projectId) {
|
const state = store.getState();
|
||||||
console.log('🔄 Refetching tasks after priority change...');
|
const currentTask = state.taskManagement.entities[response.id];
|
||||||
dispatch(fetchTasksV3(projectId));
|
|
||||||
|
if (currentTask) {
|
||||||
|
// Get priority list to map priority_id to priority name
|
||||||
|
const priorityList = state.priorityReducer?.priorities || [];
|
||||||
|
const priority = priorityList.find(p => p.id === response.priority_id);
|
||||||
|
|
||||||
|
let newPriorityValue: 'critical' | 'high' | 'medium' | 'low' = 'medium';
|
||||||
|
if (priority?.name) {
|
||||||
|
const priorityName = priority.name.toLowerCase();
|
||||||
|
if (['critical', 'high', 'medium', 'low'].includes(priorityName)) {
|
||||||
|
newPriorityValue = priorityName as 'critical' | 'high' | 'medium' | 'low';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔧 Updating task priority:', {
|
||||||
|
taskId: response.id,
|
||||||
|
oldPriority: currentTask.priority,
|
||||||
|
newPriority: newPriorityValue,
|
||||||
|
priorityId: response.priority_id,
|
||||||
|
currentGrouping: state.taskManagement.grouping
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the task entity
|
||||||
|
dispatch(updateTask({
|
||||||
|
id: response.id,
|
||||||
|
changes: {
|
||||||
|
priority: newPriorityValue,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Handle group movement ONLY if grouping by priority
|
||||||
|
const groups = state.taskManagement.groups;
|
||||||
|
const currentGrouping = state.taskManagement.grouping;
|
||||||
|
|
||||||
|
if (groups && groups.length > 0 && currentGrouping === 'priority') {
|
||||||
|
// Find current group containing the task
|
||||||
|
const currentGroup = groups.find(group => group.taskIds.includes(response.id));
|
||||||
|
|
||||||
|
// Find target group based on new priority value
|
||||||
|
const targetGroup = groups.find(group =>
|
||||||
|
group.groupValue.toLowerCase() === newPriorityValue.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) {
|
||||||
|
console.log('🔄 Moving task between priority groups:', {
|
||||||
|
taskId: response.id,
|
||||||
|
fromGroup: currentGroup.title,
|
||||||
|
toGroup: targetGroup.title,
|
||||||
|
newPriority: newPriorityValue
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(moveTaskBetweenGroups({
|
||||||
|
taskId: response.id,
|
||||||
|
fromGroupId: currentGroup.id,
|
||||||
|
toGroupId: targetGroup.id,
|
||||||
|
taskUpdate: {
|
||||||
|
priority: newPriorityValue,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
console.log('🔧 No group movement needed for priority change');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('🔧 Not grouped by priority, skipping group movement');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch, currentGroupingV3]
|
[dispatch, currentGroupingV3]
|
||||||
@@ -244,17 +365,99 @@ export const useTaskSocketHandlers = () => {
|
|||||||
(data: ITaskPhaseChangeResponse) => {
|
(data: ITaskPhaseChangeResponse) => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
|
console.log('🎯 Phase change received:', data);
|
||||||
|
|
||||||
// Update the old task slice (for backward compatibility)
|
// Update the old task slice (for backward compatibility)
|
||||||
dispatch(updateTaskPhase(data));
|
dispatch(updateTaskPhase(data));
|
||||||
dispatch(deselectAll());
|
dispatch(deselectAll());
|
||||||
|
|
||||||
// For the task management slice, refetch tasks to ensure consistency
|
// For the task management slice, always update the task entity first
|
||||||
if (data.task_id && projectId) {
|
const state = store.getState();
|
||||||
console.log('🔄 Refetching tasks after phase change...');
|
const taskId = data.task_id;
|
||||||
dispatch(fetchTasksV3(projectId));
|
|
||||||
|
if (taskId) {
|
||||||
|
const currentTask = state.taskManagement.entities[taskId];
|
||||||
|
|
||||||
|
if (currentTask) {
|
||||||
|
// Get phase list to map phase_id to phase name
|
||||||
|
const phaseList = state.phaseReducer?.phaseList || [];
|
||||||
|
let newPhaseValue = '';
|
||||||
|
|
||||||
|
if (data.id) {
|
||||||
|
// data.id is the phase_id
|
||||||
|
const phase = phaseList.find(p => p.id === data.id);
|
||||||
|
newPhaseValue = phase?.name || '';
|
||||||
|
} else {
|
||||||
|
// No phase selected (cleared)
|
||||||
|
newPhaseValue = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔧 Updating task phase:', {
|
||||||
|
taskId: taskId,
|
||||||
|
oldPhase: currentTask.phase,
|
||||||
|
newPhase: newPhaseValue,
|
||||||
|
phaseId: data.id,
|
||||||
|
currentGrouping: state.taskManagement.grouping
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the task entity
|
||||||
|
dispatch(updateTask({
|
||||||
|
id: taskId,
|
||||||
|
changes: {
|
||||||
|
phase: newPhaseValue,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Handle group movement ONLY if grouping by phase
|
||||||
|
const groups = state.taskManagement.groups;
|
||||||
|
const currentGrouping = state.taskManagement.grouping;
|
||||||
|
|
||||||
|
if (groups && groups.length > 0 && currentGrouping === 'phase') {
|
||||||
|
// Find current group containing the task
|
||||||
|
const currentGroup = groups.find(group => group.taskIds.includes(taskId));
|
||||||
|
|
||||||
|
// Find target group based on new phase value
|
||||||
|
let targetGroup: any = null;
|
||||||
|
|
||||||
|
if (newPhaseValue) {
|
||||||
|
// Find group by phase name
|
||||||
|
targetGroup = groups.find(group =>
|
||||||
|
group.groupValue === newPhaseValue || group.title === newPhaseValue
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Find "No Phase" or similar group
|
||||||
|
targetGroup = groups.find(group =>
|
||||||
|
group.groupValue === '' || group.title.toLowerCase().includes('no phase') || group.title.toLowerCase().includes('unassigned')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentGroup && targetGroup && currentGroup.id !== targetGroup.id) {
|
||||||
|
console.log('🔄 Moving task between phase groups:', {
|
||||||
|
taskId: taskId,
|
||||||
|
fromGroup: currentGroup.title,
|
||||||
|
toGroup: targetGroup.title,
|
||||||
|
newPhase: newPhaseValue || 'No Phase'
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(moveTaskBetweenGroups({
|
||||||
|
taskId: taskId,
|
||||||
|
fromGroupId: currentGroup.id,
|
||||||
|
toGroupId: targetGroup.id,
|
||||||
|
taskUpdate: {
|
||||||
|
phase: newPhaseValue,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
console.log('🔧 No group movement needed for phase change');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('🔧 Not grouped by phase, skipping group movement');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch, currentGroupingV3]
|
[dispatch, currentGroupingV3, projectId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleStartDateChange = useCallback(
|
const handleStartDateChange = useCallback(
|
||||||
@@ -355,7 +558,26 @@ export const useTaskSocketHandlers = () => {
|
|||||||
order: data.sort_order || 0,
|
order: data.sort_order || 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
dispatch(addTask(task));
|
// Extract the group UUID from the backend response based on current grouping
|
||||||
|
let groupId: string | undefined;
|
||||||
|
|
||||||
|
// Select the correct UUID based on current grouping
|
||||||
|
// If currentGroupingV3 is null, default to 'status' since that's the most common grouping
|
||||||
|
const grouping = currentGroupingV3 || 'status';
|
||||||
|
|
||||||
|
if (grouping === 'status') {
|
||||||
|
// For status grouping, use status field (which contains the status UUID)
|
||||||
|
groupId = data.status;
|
||||||
|
} else if (grouping === 'priority') {
|
||||||
|
// For priority grouping, use priority field (which contains the priority UUID)
|
||||||
|
groupId = data.priority;
|
||||||
|
} else if (grouping === 'phase') {
|
||||||
|
// For phase grouping, use phase_id
|
||||||
|
groupId = data.phase_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use addTaskToGroup with the actual group UUID
|
||||||
|
dispatch(addTaskToGroup({ task, groupId }));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
|
|||||||
Reference in New Issue
Block a user