From 037d3f4d733add61345b881a9eb7bd7292861ea1 Mon Sep 17 00:00:00 2001 From: paul-nothaft <40865108-paul-nothaft@users.noreply.replit.com> Date: Fri, 24 Oct 2025 08:50:06 +0000 Subject: [PATCH] Add right-click context menus for task and calendar interactions Integrates context menus on the task card and calendar views, providing options to edit, start/stop timers, and change task status. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/659922a9-0087-461c-90dd-6d9a58b81d4d/ceced2fc-aa46-458d-ba87-ddd4b7bb1518/R3zkvYP --- .replit | 4 + client/src/components/TaskCard.tsx | 126 +++++++++++++-- client/src/components/TasksWithCalendar.tsx | 170 ++++++++++++++++---- 3 files changed, 260 insertions(+), 40 deletions(-) diff --git a/.replit b/.replit index a572720..44b4a4e 100644 --- a/.replit +++ b/.replit @@ -14,6 +14,10 @@ run = ["npm", "run", "start"] localPort = 5000 externalPort = 80 +[[ports]] +localPort = 33497 +externalPort = 4200 + [[ports]] localPort = 35345 externalPort = 3002 diff --git a/client/src/components/TaskCard.tsx b/client/src/components/TaskCard.tsx index 1f804d3..e763336 100644 --- a/client/src/components/TaskCard.tsx +++ b/client/src/components/TaskCard.tsx @@ -9,6 +9,13 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; import { Clock, Calendar, Play, Pause, MoreHorizontal, Edit, Trash2, Timer, CheckCircle, X } from "lucide-react"; import { Task, Label } from '@shared/schema'; import { useQuery } from '@tanstack/react-query'; @@ -90,17 +97,19 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe }; return ( - + + +
{/* Checkbox for quick completion */} @@ -313,5 +322,100 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
+
+ + { + e.stopPropagation(); + onEdit?.(); + }} + data-testid={`context-menu-edit-${task.id}`} + > + + {t('taskCard.editTask')} + + + { + e.stopPropagation(); + handleToggleTimer(); + }} + data-testid={`context-menu-timer-${task.id}`} + > + + {task.isTracking ? t('taskCard.stopTimer') : t('taskCard.startTimer')} + + + + + {task.status !== 'done' && ( + { + e.stopPropagation(); + onStatusChange?.('done'); + }} + data-testid={`context-menu-complete-${task.id}`} + > + + {t('taskCard.markAsDone')} + + )} + + {task.status === 'todo' && ( + { + e.stopPropagation(); + onStatusChange?.('inProgress'); + }} + data-testid={`context-menu-start-${task.id}`} + > + + {t('taskCard.startWorking')} + + )} + + {task.status === 'inProgress' && ( + { + e.stopPropagation(); + onStatusChange?.('todo'); + }} + data-testid={`context-menu-todo-${task.id}`} + > + + {t('taskCard.moveToTodo')} + + )} + + {task.status === 'done' && ( + { + e.stopPropagation(); + onStatusChange?.('inProgress'); + }} + data-testid={`context-menu-reopen-${task.id}`} + > + + {t('taskCard.reopenTask')} + + )} + + + + {onDelete && ( + { + e.stopPropagation(); + onDelete(); + }} + className="text-destructive" + data-testid={`context-menu-delete-${task.id}`} + > + + {t('taskCard.deleteTask')} + + )} + +
); } \ No newline at end of file diff --git a/client/src/components/TasksWithCalendar.tsx b/client/src/components/TasksWithCalendar.tsx index 757556b..c52371d 100644 --- a/client/src/components/TasksWithCalendar.tsx +++ b/client/src/components/TasksWithCalendar.tsx @@ -4,12 +4,20 @@ import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Search, Filter, SortAsc, Calendar, ChevronLeft, ChevronRight } from 'lucide-react'; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "@/components/ui/context-menu"; +import { Search, Filter, SortAsc, Calendar, ChevronLeft, ChevronRight, Edit, Timer, CheckCircle, Play, Clock, Trash2 } from 'lucide-react'; import { Task, Label } from '@shared/schema'; import TaskCard from './TaskCard'; import { useQuery } from '@tanstack/react-query'; import TimeCompletionModal from './TimeCompletionModal'; import { addDays, format, isSameDay, startOfToday } from 'date-fns'; +import { useTranslation } from 'react-i18next'; interface TasksWithCalendarProps { tasks: Task[]; @@ -24,6 +32,7 @@ type SortOption = 'dueDate' | 'priority' | 'title' | 'status'; type FilterOption = 'all' | 'todo' | 'inProgress' | 'done' | 'overdue'; export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit, onTaskDelete, onStartTimer, onStopTimer }: TasksWithCalendarProps) { + const { t } = useTranslation(); const [searchQuery, setSearchQuery] = useState(''); const [sortBy, setSortBy] = useState('dueDate'); const [filterBy, setFilterBy] = useState('all'); @@ -408,35 +417,138 @@ export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit, onT const taskLabel = task.labelId && labels.length > 0 ? labels.find(label => label.id === task.labelId) : null; return ( -
handleDragStart(task.id, e)} - onDragEnd={handleDragEnd} - onClick={(e) => { - // Prevent click when dragging - if (draggedTask === task.id) return; - e.stopPropagation(); - onTaskEdit?.(task); - console.log('Calendar task clicked:', task.title); - }} - className={`cursor-pointer hover-elevate ${draggedTask === task.id ? 'opacity-50' : ''}`} - data-testid={`calendar-task-${task.id}`} - > - -
- {task.title} + + +
handleDragStart(task.id, e)} + onDragEnd={handleDragEnd} + onClick={(e) => { + // Prevent click when dragging + if (draggedTask === task.id) return; + e.stopPropagation(); + onTaskEdit?.(task); + console.log('Calendar task clicked:', task.title); + }} + className={`cursor-pointer hover-elevate ${draggedTask === task.id ? 'opacity-50' : ''}`} + data-testid={`calendar-task-${task.id}`} + > + +
+ {task.title} +
+
- -
+ + + { + e.stopPropagation(); + onTaskEdit?.(task); + }} + data-testid={`mini-context-menu-edit-${task.id}`} + > + + {t('taskCard.editTask')} + + + { + e.stopPropagation(); + if (task.isTracking) { + onStopTimer?.(task.id); + } else { + onStartTimer?.(task.id); + } + }} + data-testid={`mini-context-menu-timer-${task.id}`} + > + + {task.isTracking ? t('taskCard.stopTimer') : t('taskCard.startTimer')} + + + + + {task.status !== 'done' && ( + { + e.stopPropagation(); + const taskToComplete = tasks.find(t => t.id === task.id); + if (taskToComplete) { + handleTaskComplete(taskToComplete); + } + }} + data-testid={`mini-context-menu-complete-${task.id}`} + > + + {t('taskCard.markAsDone')} + + )} + + {task.status === 'todo' && ( + { + e.stopPropagation(); + onTaskUpdate?.(task.id, { status: 'inProgress' }); + }} + data-testid={`mini-context-menu-start-${task.id}`} + > + + {t('taskCard.startWorking')} + + )} + + {task.status === 'inProgress' && ( + { + e.stopPropagation(); + onTaskUpdate?.(task.id, { status: 'todo' }); + }} + data-testid={`mini-context-menu-todo-${task.id}`} + > + + {t('taskCard.moveToTodo')} + + )} + + {task.status === 'done' && ( + { + e.stopPropagation(); + onTaskUpdate?.(task.id, { status: 'inProgress' }); + }} + data-testid={`mini-context-menu-reopen-${task.id}`} + > + + {t('taskCard.reopenTask')} + + )} + + + + {onTaskDelete && ( + { + e.stopPropagation(); + onTaskDelete(task.id); + }} + className="text-destructive" + data-testid={`mini-context-menu-delete-${task.id}`} + > + + {t('taskCard.deleteTask')} + + )} + + ); })}