feat(task-management): enhance task row functionality and URL synchronization
- Integrated Redux for managing task drawer state, allowing for task selection and data fetching when opening the task drawer. - Improved URL synchronization logic to handle task ID updates more effectively, ensuring proper state management during drawer interactions. - Updated task indicators to use type-safe access for subtasks, comments, and attachments counts, enhancing code reliability and readability. - Refactored URL clearing logic to prevent unnecessary updates when closing the task drawer, improving user experience.
This commit is contained in:
@@ -36,6 +36,8 @@ import {
|
|||||||
taskPropsEqual
|
taskPropsEqual
|
||||||
} from './task-row-utils';
|
} from './task-row-utils';
|
||||||
import './task-row-optimized.css';
|
import './task-row-optimized.css';
|
||||||
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
|
import { setSelectedTaskId, setShowTaskDrawer, fetchTask } from '@/features/task-drawer/task-drawer.slice';
|
||||||
|
|
||||||
interface TaskRowProps {
|
interface TaskRowProps {
|
||||||
task: Task;
|
task: Task;
|
||||||
@@ -184,6 +186,9 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
// PERFORMANCE OPTIMIZATION: Only connect to socket after component is visible
|
// PERFORMANCE OPTIMIZATION: Only connect to socket after component is visible
|
||||||
const { socket, connected } = useSocket();
|
const { socket, connected } = useSocket();
|
||||||
|
|
||||||
|
// Redux dispatch
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
// Edit task name state
|
// Edit task name state
|
||||||
const [editTaskName, setEditTaskName] = useState(false);
|
const [editTaskName, setEditTaskName] = useState(false);
|
||||||
const [taskName, setTaskName] = useState(task.title || '');
|
const [taskName, setTaskName] = useState(task.title || '');
|
||||||
@@ -345,6 +350,15 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
}
|
}
|
||||||
}, [showAddSubtask]);
|
}, [showAddSubtask]);
|
||||||
|
|
||||||
|
// Handle opening task drawer
|
||||||
|
const handleOpenTask = useCallback(() => {
|
||||||
|
if (!task.id) return;
|
||||||
|
dispatch(setSelectedTaskId(task.id));
|
||||||
|
dispatch(setShowTaskDrawer(true));
|
||||||
|
// Fetch task data
|
||||||
|
dispatch(fetchTask({ taskId: task.id, projectId }));
|
||||||
|
}, [task.id, projectId, dispatch]);
|
||||||
|
|
||||||
// Optimized date handling with better memoization
|
// Optimized date handling with better memoization
|
||||||
const dateValues = useMemo(() => ({
|
const dateValues = useMemo(() => ({
|
||||||
start: task.startDate ? dayjs(task.startDate) : undefined,
|
start: task.startDate ? dayjs(task.startDate) : undefined,
|
||||||
@@ -596,8 +610,8 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
{!editTaskName && (
|
{!editTaskName && (
|
||||||
<div className="task-indicators flex items-center gap-1">
|
<div className="task-indicators flex items-center gap-1">
|
||||||
{/* Subtasks count */}
|
{/* Subtasks count */}
|
||||||
{task.subtasks_count && task.subtasks_count > 0 && (
|
{(task as any).subtasks_count && (task as any).subtasks_count > 0 && (
|
||||||
<Tooltip title={`${task.subtasks_count} ${task.subtasks_count !== 1 ? t('subtasks') : t('subtask')}`}>
|
<Tooltip title={`${(task as any).subtasks_count} ${(task as any).subtasks_count !== 1 ? t('subtasks') : t('subtask')}`}>
|
||||||
<div
|
<div
|
||||||
className={`indicator-badge subtasks flex items-center gap-1 px-1 py-0.5 rounded text-xs font-semibold cursor-pointer transition-colors duration-200 ${
|
className={`indicator-badge subtasks flex items-center gap-1 px-1 py-0.5 rounded text-xs font-semibold cursor-pointer transition-colors duration-200 ${
|
||||||
isDarkMode
|
isDarkMode
|
||||||
@@ -611,15 +625,15 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
handleToggleSubtasks?.();
|
handleToggleSubtasks?.();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>{task.subtasks_count}</span>
|
<span>{(task as any).subtasks_count}</span>
|
||||||
<RightOutlined style={{ fontSize: '8px' }} />
|
<RightOutlined style={{ fontSize: '8px' }} />
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Comments indicator */}
|
{/* Comments indicator */}
|
||||||
{task.comments_count && task.comments_count > 0 && (
|
{(task as any).comments_count && (task as any).comments_count > 0 && (
|
||||||
<Tooltip title={`${task.comments_count} ${task.comments_count !== 1 ? t('comments') : t('comment')}`}>
|
<Tooltip title={`${(task as any).comments_count} ${(task as any).comments_count !== 1 ? t('comments') : t('comment')}`}>
|
||||||
<div
|
<div
|
||||||
className={`indicator-badge comments flex items-center gap-1 px-1 py-0.5 rounded text-xs font-semibold ${
|
className={`indicator-badge comments flex items-center gap-1 px-1 py-0.5 rounded text-xs font-semibold ${
|
||||||
isDarkMode
|
isDarkMode
|
||||||
@@ -629,14 +643,14 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
style={{ fontSize: '10px', border: '1px solid' }}
|
style={{ fontSize: '10px', border: '1px solid' }}
|
||||||
>
|
>
|
||||||
<MessageOutlined style={{ fontSize: '8px' }} />
|
<MessageOutlined style={{ fontSize: '8px' }} />
|
||||||
<span>{task.comments_count}</span>
|
<span>{(task as any).comments_count}</span>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Attachments indicator */}
|
{/* Attachments indicator */}
|
||||||
{task.attachments_count && task.attachments_count > 0 && (
|
{(task as any).attachments_count && (task as any).attachments_count > 0 && (
|
||||||
<Tooltip title={`${task.attachments_count} ${task.attachments_count !== 1 ? t('attachments') : t('attachment')}`}>
|
<Tooltip title={`${(task as any).attachments_count} ${(task as any).attachments_count !== 1 ? t('attachments') : t('attachment')}`}>
|
||||||
<div
|
<div
|
||||||
className={`indicator-badge attachments flex items-center gap-1 px-1 py-0.5 rounded text-xs font-semibold ${
|
className={`indicator-badge attachments flex items-center gap-1 px-1 py-0.5 rounded text-xs font-semibold ${
|
||||||
isDarkMode
|
isDarkMode
|
||||||
@@ -646,7 +660,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
style={{ fontSize: '10px', border: '1px solid' }}
|
style={{ fontSize: '10px', border: '1px solid' }}
|
||||||
>
|
>
|
||||||
<PaperClipOutlined style={{ fontSize: '8px' }} />
|
<PaperClipOutlined style={{ fontSize: '8px' }} />
|
||||||
<span>{task.attachments_count}</span>
|
<span>{(task as any).attachments_count}</span>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
@@ -661,7 +675,7 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// Handle opening task drawer
|
handleOpenTask();
|
||||||
}}
|
}}
|
||||||
className={`flex items-center gap-1 px-2 py-1 rounded border transition-all duration-200 text-xs font-medium ${
|
className={`flex items-center gap-1 px-2 py-1 rounded border transition-all duration-200 text-xs font-medium ${
|
||||||
isDarkMode
|
isDarkMode
|
||||||
@@ -956,10 +970,9 @@ const TaskRow: React.FC<TaskRowProps> = React.memo(({
|
|||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
|
||||||
onClick={handleAddSubtask}
|
onClick={handleAddSubtask}
|
||||||
disabled={!newSubtaskName.trim()}
|
disabled={!newSubtaskName.trim()}
|
||||||
className="h-6 px-2 text-xs"
|
className="h-6 px-2 text-xs bg-blue-500 text-white hover:bg-blue-600"
|
||||||
>
|
>
|
||||||
{t('add')}
|
{t('add')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -18,16 +18,17 @@ const useTaskDrawerUrlSync = () => {
|
|||||||
|
|
||||||
// Use a ref to track whether we're in the process of closing the drawer
|
// Use a ref to track whether we're in the process of closing the drawer
|
||||||
const isClosingDrawer = useRef(false);
|
const isClosingDrawer = useRef(false);
|
||||||
// Use a ref to track if we've already processed the initial URL
|
|
||||||
const initialUrlProcessed = useRef(false);
|
|
||||||
// Use a ref to track the last task ID we processed
|
// Use a ref to track the last task ID we processed
|
||||||
const lastProcessedTaskId = useRef<string | null>(null);
|
const lastProcessedTaskId = useRef<string | null>(null);
|
||||||
|
// Use a ref to track if we should ignore URL changes (when programmatically updating)
|
||||||
|
const shouldIgnoreUrlChange = useRef(false);
|
||||||
|
|
||||||
// Function to clear the task parameter from URL
|
// Function to clear the task parameter from URL
|
||||||
const clearTaskFromUrl = useCallback(() => {
|
const clearTaskFromUrl = useCallback(() => {
|
||||||
if (searchParams.has('task')) {
|
if (searchParams.has('task')) {
|
||||||
// Set the flag to indicate we're closing the drawer
|
// Set the flag to indicate we're closing the drawer
|
||||||
isClosingDrawer.current = true;
|
isClosingDrawer.current = true;
|
||||||
|
shouldIgnoreUrlChange.current = true;
|
||||||
|
|
||||||
// Create a new URLSearchParams object to avoid modifying the current one
|
// Create a new URLSearchParams object to avoid modifying the current one
|
||||||
const newParams = new URLSearchParams(searchParams);
|
const newParams = new URLSearchParams(searchParams);
|
||||||
@@ -36,19 +37,32 @@ const useTaskDrawerUrlSync = () => {
|
|||||||
// Update the URL without triggering a navigation
|
// Update the URL without triggering a navigation
|
||||||
setSearchParams(newParams, { replace: true });
|
setSearchParams(newParams, { replace: true });
|
||||||
|
|
||||||
// Reset the flag after a short delay
|
// Reset the flags after a short delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isClosingDrawer.current = false;
|
isClosingDrawer.current = false;
|
||||||
}, 200);
|
shouldIgnoreUrlChange.current = false;
|
||||||
|
}, 300); // Increased timeout to ensure proper cleanup
|
||||||
}
|
}
|
||||||
}, [searchParams, setSearchParams]);
|
}, [searchParams, setSearchParams]);
|
||||||
|
|
||||||
// Check for task ID in URL when component mounts
|
// Check for task ID in URL when it changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only process the URL once on initial mount
|
// Skip if we're programmatically updating the URL or closing the drawer
|
||||||
if (!initialUrlProcessed.current) {
|
if (shouldIgnoreUrlChange.current || isClosingDrawer.current) return;
|
||||||
|
|
||||||
const taskIdFromUrl = searchParams.get('task');
|
const taskIdFromUrl = searchParams.get('task');
|
||||||
if (taskIdFromUrl && !showTaskDrawer && projectId && !isClosingDrawer.current) {
|
|
||||||
|
// Only process URL changes if:
|
||||||
|
// 1. There's a task ID in the URL
|
||||||
|
// 2. The drawer is not currently open
|
||||||
|
// 3. We have a project ID
|
||||||
|
// 4. It's a different task ID than what we last processed
|
||||||
|
// 5. The selected task ID is different from URL (to avoid reopening same task)
|
||||||
|
if (taskIdFromUrl &&
|
||||||
|
!showTaskDrawer &&
|
||||||
|
projectId &&
|
||||||
|
taskIdFromUrl !== lastProcessedTaskId.current &&
|
||||||
|
taskIdFromUrl !== selectedTaskId) {
|
||||||
lastProcessedTaskId.current = taskIdFromUrl;
|
lastProcessedTaskId.current = taskIdFromUrl;
|
||||||
dispatch(setSelectedTaskId(taskIdFromUrl));
|
dispatch(setSelectedTaskId(taskIdFromUrl));
|
||||||
dispatch(setShowTaskDrawer(true));
|
dispatch(setShowTaskDrawer(true));
|
||||||
@@ -56,20 +70,19 @@ const useTaskDrawerUrlSync = () => {
|
|||||||
// Fetch task data
|
// Fetch task data
|
||||||
dispatch(fetchTask({ taskId: taskIdFromUrl, projectId }));
|
dispatch(fetchTask({ taskId: taskIdFromUrl, projectId }));
|
||||||
}
|
}
|
||||||
initialUrlProcessed.current = true;
|
}, [searchParams, showTaskDrawer, projectId, selectedTaskId, dispatch]);
|
||||||
}
|
|
||||||
}, [searchParams, dispatch, showTaskDrawer, projectId]);
|
|
||||||
|
|
||||||
// Update URL when task drawer state changes
|
// Update URL when task drawer state changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Don't update URL if we're in the process of closing
|
// Don't update URL if we're in the process of closing or ignoring changes
|
||||||
if (isClosingDrawer.current) return;
|
if (isClosingDrawer.current || shouldIgnoreUrlChange.current) return;
|
||||||
|
|
||||||
if (showTaskDrawer && selectedTaskId) {
|
if (showTaskDrawer && selectedTaskId) {
|
||||||
// Don't update if it's the same task ID we already processed
|
// Don't update if it's the same task ID we already processed
|
||||||
if (lastProcessedTaskId.current === selectedTaskId) return;
|
if (lastProcessedTaskId.current === selectedTaskId) return;
|
||||||
|
|
||||||
// Add task ID to URL when drawer is opened
|
// Add task ID to URL when drawer is opened
|
||||||
|
shouldIgnoreUrlChange.current = true;
|
||||||
lastProcessedTaskId.current = selectedTaskId;
|
lastProcessedTaskId.current = selectedTaskId;
|
||||||
|
|
||||||
// Create a new URLSearchParams object to avoid modifying the current one
|
// Create a new URLSearchParams object to avoid modifying the current one
|
||||||
@@ -78,12 +91,27 @@ const useTaskDrawerUrlSync = () => {
|
|||||||
|
|
||||||
// Update the URL without triggering a navigation
|
// Update the URL without triggering a navigation
|
||||||
setSearchParams(newParams, { replace: true });
|
setSearchParams(newParams, { replace: true });
|
||||||
} else if (!showTaskDrawer && searchParams.has('task')) {
|
|
||||||
// Remove task ID from URL when drawer is closed
|
// Reset the flag after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
shouldIgnoreUrlChange.current = false;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}, [showTaskDrawer, selectedTaskId, searchParams, setSearchParams]);
|
||||||
|
|
||||||
|
// Separate effect to handle URL clearing when drawer is closed
|
||||||
|
useEffect(() => {
|
||||||
|
// Only clear URL when drawer is closed and we have a task in URL
|
||||||
|
// Also ensure we're not in the middle of processing other URL changes
|
||||||
|
if (!showTaskDrawer &&
|
||||||
|
searchParams.has('task') &&
|
||||||
|
!isClosingDrawer.current &&
|
||||||
|
!shouldIgnoreUrlChange.current &&
|
||||||
|
!selectedTaskId) { // Only clear if selectedTaskId is also null/cleared
|
||||||
clearTaskFromUrl();
|
clearTaskFromUrl();
|
||||||
lastProcessedTaskId.current = null;
|
lastProcessedTaskId.current = null;
|
||||||
}
|
}
|
||||||
}, [showTaskDrawer, selectedTaskId, searchParams, setSearchParams, clearTaskFromUrl]);
|
}, [showTaskDrawer, searchParams, selectedTaskId, clearTaskFromUrl]);
|
||||||
|
|
||||||
return { clearTaskFromUrl };
|
return { clearTaskFromUrl };
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user