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:
chamikaJ
2025-06-27 13:56:46 +05:30
parent e74668c389
commit 8b63c1cf9e
2 changed files with 76 additions and 35 deletions

View File

@@ -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>

View File

@@ -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 };
}; };