4ea8a7c6a1
Update TaskCard and TaskCreationModal components to support task labels, including UI elements for selection and display. Refactor schema imports to use '@shared/schema'. 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/vtpluZY
192 lines
6.3 KiB
TypeScript
192 lines
6.3 KiB
TypeScript
import { useState } from 'react';
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { Calendar } from '@/components/ui/calendar';
|
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
import { CalendarIcon, Plus, Tag } from 'lucide-react';
|
|
import { Task, Label } from '@shared/schema';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
|
|
interface TaskCreationModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onSave: (task: Partial<Task>) => void;
|
|
}
|
|
|
|
export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreationModalProps) {
|
|
const [title, setTitle] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [priority, setPriority] = useState<'low' | 'medium' | 'high'>('medium');
|
|
const [dueDate, setDueDate] = useState<Date | undefined>();
|
|
const [labelId, setLabelId] = useState<string | undefined>();
|
|
const [isCalendarOpen, setIsCalendarOpen] = useState(false);
|
|
|
|
// Fetch labels
|
|
const { data: labels = [] } = useQuery<Label[]>({
|
|
queryKey: ['/api/labels'],
|
|
});
|
|
|
|
const handleSave = () => {
|
|
if (!title.trim()) return;
|
|
|
|
const newTask: Partial<Task> = {
|
|
title: title.trim(),
|
|
description: description.trim() || undefined,
|
|
priority,
|
|
dueDate,
|
|
labelId,
|
|
status: 'todo',
|
|
timeTracked: 0,
|
|
isTracking: false
|
|
};
|
|
|
|
onSave(newTask);
|
|
console.log('New task created:', newTask);
|
|
|
|
// Reset form
|
|
setTitle('');
|
|
setDescription('');
|
|
setPriority('medium');
|
|
setDueDate(undefined);
|
|
setLabelId(undefined);
|
|
onClose();
|
|
};
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
handleSave();
|
|
}
|
|
};
|
|
|
|
const handleClose = () => {
|
|
setTitle('');
|
|
setDescription('');
|
|
setPriority('medium');
|
|
setDueDate(undefined);
|
|
setLabelId(undefined);
|
|
onClose();
|
|
};
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
|
<DialogContent className="sm:max-w-md mx-4">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<Plus className="w-4 h-4" />
|
|
New Task
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Input
|
|
placeholder="What needs to be done?"
|
|
value={title}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
className="text-base"
|
|
data-testid="input-task-title"
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Textarea
|
|
placeholder="Add a description (optional)"
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
className="resize-none text-sm"
|
|
rows={3}
|
|
data-testid="input-task-description"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-3">
|
|
<div className="flex-1">
|
|
<Select value={priority} onValueChange={(value: 'low' | 'medium' | 'high') => setPriority(value)}>
|
|
<SelectTrigger data-testid="select-task-priority">
|
|
<SelectValue placeholder="Priority" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="low">Low</SelectItem>
|
|
<SelectItem value="medium">Medium</SelectItem>
|
|
<SelectItem value="high">High</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex-1">
|
|
<Select value={labelId || 'none'} onValueChange={(value) => setLabelId(value === 'none' ? undefined : value)}>
|
|
<SelectTrigger data-testid="select-task-label">
|
|
<SelectValue placeholder="Label" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="none">No Label</SelectItem>
|
|
{labels.map((label) => (
|
|
<SelectItem key={label.id} value={label.id}>
|
|
<div className="flex items-center gap-2">
|
|
<div
|
|
className="w-3 h-3 rounded"
|
|
style={{ backgroundColor: label.color }}
|
|
/>
|
|
{label.name}
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
className="flex items-center gap-2"
|
|
data-testid="button-due-date"
|
|
>
|
|
<CalendarIcon className="w-4 h-4" />
|
|
{dueDate ? dueDate.toLocaleDateString() : 'Due date'}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-auto p-0" align="end">
|
|
<Calendar
|
|
mode="single"
|
|
selected={dueDate}
|
|
onSelect={(date) => {
|
|
setDueDate(date);
|
|
setIsCalendarOpen(false);
|
|
}}
|
|
disabled={(date) => date < new Date()}
|
|
initialFocus
|
|
/>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
<div className="flex gap-3 pt-2">
|
|
<Button
|
|
variant="outline"
|
|
onClick={handleClose}
|
|
className="flex-1"
|
|
data-testid="button-cancel"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={handleSave}
|
|
disabled={!title.trim()}
|
|
className="flex-1"
|
|
data-testid="button-save-task"
|
|
>
|
|
Create Task
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
} |