Add label system for tasks with custom colors and management options
Introduce label creation, editing, and deletion functionality to the task management system, enabling users to assign colored labels to tasks. 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/XrG8SoT
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -7,8 +7,12 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from
|
||||
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 { Plus, Calendar as CalendarIcon, Clock, Copy, Play } from 'lucide-react';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Plus, Calendar as CalendarIcon, Clock, Copy, Play, Tag, Edit, Trash2, Palette } from 'lucide-react';
|
||||
import { Task } from './TaskCard';
|
||||
import { Label } from '@shared/schema';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiRequest } from '@/lib/queryClient';
|
||||
|
||||
interface TemplateTask {
|
||||
id: string;
|
||||
@@ -74,6 +78,53 @@ export default function ProjectTemplate({
|
||||
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
||||
const [isLaunchOpen, setIsLaunchOpen] = useState(false);
|
||||
const [isCalendarOpen, setIsCalendarOpen] = useState(false);
|
||||
|
||||
// Label management state
|
||||
const [isLabelDialogOpen, setIsLabelDialogOpen] = useState(false);
|
||||
const [editingLabel, setEditingLabel] = useState<Label | null>(null);
|
||||
const [labelName, setLabelName] = useState('');
|
||||
const [labelColor, setLabelColor] = useState('#3B82F6');
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Fetch labels
|
||||
const { data: labels = [], isLoading: labelsLoading } = useQuery({
|
||||
queryKey: ['/api/labels'],
|
||||
});
|
||||
|
||||
// Create label mutation
|
||||
const createLabelMutation = useMutation({
|
||||
mutationFn: (data: { name: string; color: string }) =>
|
||||
apiRequest('POST', '/api/labels', data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/labels'] });
|
||||
setIsLabelDialogOpen(false);
|
||||
setLabelName('');
|
||||
setLabelColor('#3B82F6');
|
||||
setEditingLabel(null);
|
||||
}
|
||||
});
|
||||
|
||||
// Update label mutation
|
||||
const updateLabelMutation = useMutation({
|
||||
mutationFn: ({ id, ...data }: { id: string; name?: string; color?: string }) =>
|
||||
apiRequest('PATCH', `/api/labels/${id}`, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/labels'] });
|
||||
setIsLabelDialogOpen(false);
|
||||
setLabelName('');
|
||||
setLabelColor('#3B82F6');
|
||||
setEditingLabel(null);
|
||||
}
|
||||
});
|
||||
|
||||
// Delete label mutation
|
||||
const deleteLabelMutation = useMutation({
|
||||
mutationFn: (id: string) => apiRequest('DELETE', `/api/labels/${id}`),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/labels'] });
|
||||
}
|
||||
});
|
||||
|
||||
const handleLaunchProject = () => {
|
||||
if (selectedTemplate && startDate && projectName.trim()) {
|
||||
@@ -90,6 +141,30 @@ export default function ProjectTemplate({
|
||||
setIsLaunchOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Label management functions
|
||||
const handleSaveLabel = () => {
|
||||
if (!labelName.trim()) return;
|
||||
|
||||
if (editingLabel) {
|
||||
updateLabelMutation.mutate({ id: editingLabel.id, name: labelName.trim(), color: labelColor });
|
||||
} else {
|
||||
createLabelMutation.mutate({ name: labelName.trim(), color: labelColor });
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditLabel = (label: Label) => {
|
||||
setEditingLabel(label);
|
||||
setLabelName(label.name);
|
||||
setLabelColor(label.color);
|
||||
setIsLabelDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteLabel = (id: string) => {
|
||||
if (confirm('Are you sure you want to delete this label?')) {
|
||||
deleteLabelMutation.mutate(id);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDayOffset = (offset: number) => {
|
||||
if (offset === 0) return 'Launch day';
|
||||
@@ -114,27 +189,38 @@ export default function ProjectTemplate({
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold" data-testid="text-templates-title">
|
||||
Project Templates
|
||||
Settings & Templates
|
||||
</h2>
|
||||
|
||||
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" data-testid="button-create-template">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create Template
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Template</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="p-4 text-center text-muted-foreground">
|
||||
Template creation form would go here
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="templates" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="templates" data-testid="tab-templates">Templates</TabsTrigger>
|
||||
<TabsTrigger value="labels" data-testid="tab-labels">Labels</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="templates" className="space-y-6">
|
||||
{/* Templates Section Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-md font-medium">Project Templates</h3>
|
||||
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" data-testid="button-create-template">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create Template
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Template</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="p-4 text-center text-muted-foreground">
|
||||
Template creation form would go here
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Templates by Category */}
|
||||
{getTemplatesByCategory().map(({ category, templates: categoryTemplates }) => (
|
||||
<div key={category} className="space-y-3">
|
||||
@@ -305,6 +391,146 @@ export default function ProjectTemplate({
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="labels" className="space-y-6">
|
||||
{/* Labels Section Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-md font-medium">Task Labels</h3>
|
||||
<Dialog open={isLabelDialogOpen} onOpenChange={setIsLabelDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" data-testid="button-create-label">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create Label
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingLabel ? 'Edit Label' : 'Create New Label'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Input
|
||||
placeholder="Label name"
|
||||
value={labelName}
|
||||
onChange={(e) => setLabelName(e.target.value)}
|
||||
data-testid="input-label-name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="color"
|
||||
value={labelColor}
|
||||
onChange={(e) => setLabelColor(e.target.value)}
|
||||
className="w-12 h-8 rounded border cursor-pointer"
|
||||
data-testid="input-label-color"
|
||||
/>
|
||||
<Input
|
||||
value={labelColor}
|
||||
onChange={(e) => setLabelColor(e.target.value)}
|
||||
placeholder="#3B82F6"
|
||||
className="flex-1"
|
||||
data-testid="input-label-color-text"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsLabelDialogOpen(false);
|
||||
setEditingLabel(null);
|
||||
setLabelName('');
|
||||
setLabelColor('#3B82F6');
|
||||
}}
|
||||
className="flex-1"
|
||||
data-testid="button-cancel-label"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveLabel}
|
||||
disabled={!labelName.trim() || createLabelMutation.isPending || updateLabelMutation.isPending}
|
||||
className="flex-1"
|
||||
data-testid="button-save-label"
|
||||
>
|
||||
{editingLabel ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Labels List */}
|
||||
{labelsLoading ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="text-muted-foreground">Loading labels...</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{labels.map((label: Label) => (
|
||||
<Card
|
||||
key={label.id}
|
||||
className="p-4 hover-elevate active-elevate-2"
|
||||
style={{ borderLeft: `4px solid ${label.color}` }}
|
||||
data-testid={`label-${label.id}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="w-4 h-4 rounded"
|
||||
style={{ backgroundColor: label.color }}
|
||||
/>
|
||||
<span className="font-medium text-sm" data-testid={`text-label-name-${label.id}`}>
|
||||
{label.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleEditLabel(label)}
|
||||
className="w-6 h-6"
|
||||
data-testid={`button-edit-${label.id}`}
|
||||
>
|
||||
<Edit className="w-3 h-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteLabel(label.id)}
|
||||
className="w-6 h-6 text-destructive hover:text-destructive"
|
||||
data-testid={`button-delete-${label.id}`}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
{labels.length === 0 && (
|
||||
<div className="col-span-full flex flex-col items-center justify-center p-8 text-center">
|
||||
<Tag className="w-12 h-12 text-muted-foreground mb-4" />
|
||||
<h3 className="font-medium text-muted-foreground mb-2">No labels yet</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Create your first label to organize your tasks by color and category.
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsLabelDialogOpen(true)}
|
||||
data-testid="button-create-first-label"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create Label
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user