Add ability to manage task labels and create project templates
Refactors the settings page to include CRUD operations for task labels, integrates template creation navigation, and removes the appearance section. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/659922a9-0087-461c-90dd-6d9a58b81d4d/ceced2fc-aa46-458d-ba87-ddd4b7bb1518/LNMKOrA
This commit is contained in:
+253
-16
@@ -1,10 +1,28 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Globe } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Globe, Tag, Plus, Edit, Trash2, Layers } from 'lucide-react';
|
||||
import { Label } from '@shared/schema';
|
||||
import { queryClient, apiRequest } from '@/lib/queryClient';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
export default function Settings() {
|
||||
interface SettingsProps {
|
||||
onNavigateToTemplates: () => void;
|
||||
}
|
||||
|
||||
export default function Settings({ onNavigateToTemplates }: SettingsProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [isLabelDialogOpen, setIsLabelDialogOpen] = useState(false);
|
||||
const [editingLabel, setEditingLabel] = useState<Label | null>(null);
|
||||
const [labelName, setLabelName] = useState('');
|
||||
const [labelColor, setLabelColor] = useState('#3B82F6');
|
||||
|
||||
const changeLanguage = (lng: string) => {
|
||||
i18n.changeLanguage(lng);
|
||||
@@ -12,6 +30,83 @@ export default function Settings() {
|
||||
console.log('Language changed to:', lng);
|
||||
};
|
||||
|
||||
// Fetch labels
|
||||
const { data: labels = [], isLoading: labelsLoading } = useQuery<Label[]>({
|
||||
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');
|
||||
toast({
|
||||
title: t('settings.labels.created'),
|
||||
description: t('settings.labels.createdDescription'),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// 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);
|
||||
setEditingLabel(null);
|
||||
setLabelName('');
|
||||
setLabelColor('#3B82F6');
|
||||
toast({
|
||||
title: t('settings.labels.updated'),
|
||||
description: t('settings.labels.updatedDescription'),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Delete label mutation
|
||||
const deleteLabelMutation = useMutation({
|
||||
mutationFn: (id: string) => apiRequest('DELETE', `/api/labels/${id}`),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/labels'] });
|
||||
toast({
|
||||
title: t('settings.labels.deleted'),
|
||||
description: t('settings.labels.deletedDescription'),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleSaveLabel = () => {
|
||||
if (!labelName.trim()) return;
|
||||
|
||||
if (editingLabel) {
|
||||
updateLabelMutation.mutate({
|
||||
id: editingLabel.id,
|
||||
name: labelName,
|
||||
color: labelColor,
|
||||
});
|
||||
} else {
|
||||
createLabelMutation.mutate({ name: labelName, color: labelColor });
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditLabel = (label: Label) => {
|
||||
setEditingLabel(label);
|
||||
setLabelName(label.name);
|
||||
setLabelColor(label.color);
|
||||
setIsLabelDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteLabel = (id: string) => {
|
||||
if (confirm(t('settings.labels.deleteConfirm'))) {
|
||||
deleteLabelMutation.mutate(id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
@@ -48,33 +143,175 @@ export default function Settings() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Appearance Settings */}
|
||||
<Card>
|
||||
{/* Labels Management */}
|
||||
<Card data-testid="card-labels-management">
|
||||
<CardHeader>
|
||||
<CardTitle>{t('settings.appearance.title')}</CardTitle>
|
||||
<CardDescription>
|
||||
{t('settings.appearance.description')}
|
||||
</CardDescription>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Tag className="w-5 h-5" />
|
||||
{t('settings.labels.title')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('settings.labels.description')}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<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" />
|
||||
{t('settings.labels.createLabel')}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingLabel ? t('settings.labels.editLabel') : t('settings.labels.createNewLabel')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Input
|
||||
placeholder={t('settings.labels.labelNamePlaceholder')}
|
||||
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"
|
||||
>
|
||||
{t('settings.labels.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveLabel}
|
||||
disabled={!labelName.trim() || createLabelMutation.isPending || updateLabelMutation.isPending}
|
||||
className="flex-1"
|
||||
data-testid="button-save-label"
|
||||
>
|
||||
{editingLabel ? t('settings.labels.update') : t('settings.labels.create')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('settings.appearance.themeToggle')}
|
||||
</p>
|
||||
{labelsLoading ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="text-muted-foreground">{t('settings.labels.loading')}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{labels.map((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">{t('settings.labels.noLabels')}</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{t('settings.labels.noLabelsDescription')}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsLabelDialogOpen(true)}
|
||||
data-testid="button-create-first-label"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
{t('settings.labels.createLabel')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Project Templates */}
|
||||
<Card>
|
||||
<Card data-testid="card-project-templates">
|
||||
<CardHeader>
|
||||
<CardTitle>{t('settings.templates.title')}</CardTitle>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Layers className="w-5 h-5" />
|
||||
{t('settings.templates.title')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('settings.templates.description')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('settings.templates.accessInfo')}
|
||||
</p>
|
||||
<Button
|
||||
onClick={onNavigateToTemplates}
|
||||
data-testid="button-manage-templates"
|
||||
>
|
||||
<Layers className="w-4 h-4 mr-2" />
|
||||
{t('settings.templates.manageTemplates')}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user