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
This commit is contained in:
@@ -14,6 +14,10 @@ run = ["npm", "run", "start"]
|
|||||||
localPort = 5000
|
localPort = 5000
|
||||||
externalPort = 80
|
externalPort = 80
|
||||||
|
|
||||||
|
[[ports]]
|
||||||
|
localPort = 33497
|
||||||
|
externalPort = 4200
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 35345
|
localPort = 35345
|
||||||
externalPort = 3002
|
externalPort = 3002
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} 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 { Clock, Calendar, Play, Pause, MoreHorizontal, Edit, Trash2, Timer, CheckCircle, X } from "lucide-react";
|
||||||
import { Task, Label } from '@shared/schema';
|
import { Task, Label } from '@shared/schema';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
@@ -90,17 +97,19 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<ContextMenu>
|
||||||
className={`p-4 hover-elevate active-elevate-2 transition-all duration-200 relative ${
|
<ContextMenuTrigger asChild>
|
||||||
isDragging ? 'rotate-1 scale-105 shadow-lg' : ''
|
<Card
|
||||||
} ${task.status === 'done' ? 'opacity-60' : ''}`}
|
className={`p-4 hover-elevate active-elevate-2 transition-all duration-200 relative ${
|
||||||
style={taskLabel ? {
|
isDragging ? 'rotate-1 scale-105 shadow-lg' : ''
|
||||||
borderColor: taskLabel.color,
|
} ${task.status === 'done' ? 'opacity-60' : ''}`}
|
||||||
borderWidth: '2px',
|
style={taskLabel ? {
|
||||||
borderStyle: 'solid'
|
borderColor: taskLabel.color,
|
||||||
} : {}}
|
borderWidth: '2px',
|
||||||
data-testid={`card-task-${task.id}`}
|
borderStyle: 'solid'
|
||||||
>
|
} : {}}
|
||||||
|
data-testid={`card-task-${task.id}`}
|
||||||
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
{/* Checkbox for quick completion */}
|
{/* Checkbox for quick completion */}
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -313,5 +322,100 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
</ContextMenuTrigger>
|
||||||
|
<ContextMenuContent>
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onEdit?.();
|
||||||
|
}}
|
||||||
|
data-testid={`context-menu-edit-${task.id}`}
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4 mr-2" />
|
||||||
|
{t('taskCard.editTask')}
|
||||||
|
</ContextMenuItem>
|
||||||
|
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleToggleTimer();
|
||||||
|
}}
|
||||||
|
data-testid={`context-menu-timer-${task.id}`}
|
||||||
|
>
|
||||||
|
<Timer className="w-4 h-4 mr-2" />
|
||||||
|
{task.isTracking ? t('taskCard.stopTimer') : t('taskCard.startTimer')}
|
||||||
|
</ContextMenuItem>
|
||||||
|
|
||||||
|
<ContextMenuSeparator />
|
||||||
|
|
||||||
|
{task.status !== 'done' && (
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onStatusChange?.('done');
|
||||||
|
}}
|
||||||
|
data-testid={`context-menu-complete-${task.id}`}
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
|
{t('taskCard.markAsDone')}
|
||||||
|
</ContextMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{task.status === 'todo' && (
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onStatusChange?.('inProgress');
|
||||||
|
}}
|
||||||
|
data-testid={`context-menu-start-${task.id}`}
|
||||||
|
>
|
||||||
|
<Play className="w-4 h-4 mr-2" />
|
||||||
|
{t('taskCard.startWorking')}
|
||||||
|
</ContextMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{task.status === 'inProgress' && (
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onStatusChange?.('todo');
|
||||||
|
}}
|
||||||
|
data-testid={`context-menu-todo-${task.id}`}
|
||||||
|
>
|
||||||
|
<Clock className="w-4 h-4 mr-2" />
|
||||||
|
{t('taskCard.moveToTodo')}
|
||||||
|
</ContextMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{task.status === 'done' && (
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onStatusChange?.('inProgress');
|
||||||
|
}}
|
||||||
|
data-testid={`context-menu-reopen-${task.id}`}
|
||||||
|
>
|
||||||
|
<Play className="w-4 h-4 mr-2" />
|
||||||
|
{t('taskCard.reopenTask')}
|
||||||
|
</ContextMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ContextMenuSeparator />
|
||||||
|
|
||||||
|
{onDelete && (
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDelete();
|
||||||
|
}}
|
||||||
|
className="text-destructive"
|
||||||
|
data-testid={`context-menu-delete-${task.id}`}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
|
{t('taskCard.deleteTask')}
|
||||||
|
</ContextMenuItem>
|
||||||
|
)}
|
||||||
|
</ContextMenuContent>
|
||||||
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4,12 +4,20 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
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 { Task, Label } from '@shared/schema';
|
||||||
import TaskCard from './TaskCard';
|
import TaskCard from './TaskCard';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import TimeCompletionModal from './TimeCompletionModal';
|
import TimeCompletionModal from './TimeCompletionModal';
|
||||||
import { addDays, format, isSameDay, startOfToday } from 'date-fns';
|
import { addDays, format, isSameDay, startOfToday } from 'date-fns';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface TasksWithCalendarProps {
|
interface TasksWithCalendarProps {
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
@@ -24,6 +32,7 @@ type SortOption = 'dueDate' | 'priority' | 'title' | 'status';
|
|||||||
type FilterOption = 'all' | 'todo' | 'inProgress' | 'done' | 'overdue';
|
type FilterOption = 'all' | 'todo' | 'inProgress' | 'done' | 'overdue';
|
||||||
|
|
||||||
export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit, onTaskDelete, onStartTimer, onStopTimer }: TasksWithCalendarProps) {
|
export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit, onTaskDelete, onStartTimer, onStopTimer }: TasksWithCalendarProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [sortBy, setSortBy] = useState<SortOption>('dueDate');
|
const [sortBy, setSortBy] = useState<SortOption>('dueDate');
|
||||||
const [filterBy, setFilterBy] = useState<FilterOption>('all');
|
const [filterBy, setFilterBy] = useState<FilterOption>('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;
|
const taskLabel = task.labelId && labels.length > 0 ? labels.find(label => label.id === task.labelId) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<ContextMenu key={task.id}>
|
||||||
key={task.id}
|
<ContextMenuTrigger asChild>
|
||||||
draggable
|
<div
|
||||||
onDragStart={(e) => handleDragStart(task.id, e)}
|
draggable
|
||||||
onDragEnd={handleDragEnd}
|
onDragStart={(e) => handleDragStart(task.id, e)}
|
||||||
onClick={(e) => {
|
onDragEnd={handleDragEnd}
|
||||||
// Prevent click when dragging
|
onClick={(e) => {
|
||||||
if (draggedTask === task.id) return;
|
// Prevent click when dragging
|
||||||
e.stopPropagation();
|
if (draggedTask === task.id) return;
|
||||||
onTaskEdit?.(task);
|
e.stopPropagation();
|
||||||
console.log('Calendar task clicked:', task.title);
|
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}`}
|
className={`cursor-pointer hover-elevate ${draggedTask === task.id ? 'opacity-50' : ''}`}
|
||||||
>
|
data-testid={`calendar-task-${task.id}`}
|
||||||
<Badge
|
>
|
||||||
variant="outline"
|
<Badge
|
||||||
className="w-full justify-start text-xs p-1 sm:p-1 py-2 sm:py-1 h-auto text-left"
|
variant="outline"
|
||||||
style={taskLabel ? {
|
className="w-full justify-start text-xs p-1 sm:p-1 py-2 sm:py-1 h-auto text-left"
|
||||||
borderColor: taskLabel.color,
|
style={taskLabel ? {
|
||||||
borderWidth: '2px',
|
borderColor: taskLabel.color,
|
||||||
borderStyle: 'solid'
|
borderWidth: '2px',
|
||||||
} : {}}
|
borderStyle: 'solid'
|
||||||
>
|
} : {}}
|
||||||
<div className="truncate text-sm sm:text-xs">
|
>
|
||||||
{task.title}
|
<div className="truncate text-sm sm:text-xs pointer-events-none">
|
||||||
|
{task.title}
|
||||||
|
</div>
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</Badge>
|
</ContextMenuTrigger>
|
||||||
</div>
|
<ContextMenuContent>
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onTaskEdit?.(task);
|
||||||
|
}}
|
||||||
|
data-testid={`mini-context-menu-edit-${task.id}`}
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4 mr-2" />
|
||||||
|
{t('taskCard.editTask')}
|
||||||
|
</ContextMenuItem>
|
||||||
|
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (task.isTracking) {
|
||||||
|
onStopTimer?.(task.id);
|
||||||
|
} else {
|
||||||
|
onStartTimer?.(task.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
data-testid={`mini-context-menu-timer-${task.id}`}
|
||||||
|
>
|
||||||
|
<Timer className="w-4 h-4 mr-2" />
|
||||||
|
{task.isTracking ? t('taskCard.stopTimer') : t('taskCard.startTimer')}
|
||||||
|
</ContextMenuItem>
|
||||||
|
|
||||||
|
<ContextMenuSeparator />
|
||||||
|
|
||||||
|
{task.status !== 'done' && (
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const taskToComplete = tasks.find(t => t.id === task.id);
|
||||||
|
if (taskToComplete) {
|
||||||
|
handleTaskComplete(taskToComplete);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
data-testid={`mini-context-menu-complete-${task.id}`}
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
|
{t('taskCard.markAsDone')}
|
||||||
|
</ContextMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{task.status === 'todo' && (
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onTaskUpdate?.(task.id, { status: 'inProgress' });
|
||||||
|
}}
|
||||||
|
data-testid={`mini-context-menu-start-${task.id}`}
|
||||||
|
>
|
||||||
|
<Play className="w-4 h-4 mr-2" />
|
||||||
|
{t('taskCard.startWorking')}
|
||||||
|
</ContextMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{task.status === 'inProgress' && (
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onTaskUpdate?.(task.id, { status: 'todo' });
|
||||||
|
}}
|
||||||
|
data-testid={`mini-context-menu-todo-${task.id}`}
|
||||||
|
>
|
||||||
|
<Clock className="w-4 h-4 mr-2" />
|
||||||
|
{t('taskCard.moveToTodo')}
|
||||||
|
</ContextMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{task.status === 'done' && (
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onTaskUpdate?.(task.id, { status: 'inProgress' });
|
||||||
|
}}
|
||||||
|
data-testid={`mini-context-menu-reopen-${task.id}`}
|
||||||
|
>
|
||||||
|
<Play className="w-4 h-4 mr-2" />
|
||||||
|
{t('taskCard.reopenTask')}
|
||||||
|
</ContextMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ContextMenuSeparator />
|
||||||
|
|
||||||
|
{onTaskDelete && (
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onTaskDelete(task.id);
|
||||||
|
}}
|
||||||
|
className="text-destructive"
|
||||||
|
data-testid={`mini-context-menu-delete-${task.id}`}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
|
{t('taskCard.deleteTask')}
|
||||||
|
</ContextMenuItem>
|
||||||
|
)}
|
||||||
|
</ContextMenuContent>
|
||||||
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user