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:
@@ -18,10 +18,6 @@ externalPort = 80
|
|||||||
localPort = 35345
|
localPort = 35345
|
||||||
externalPort = 3002
|
externalPort = 3002
|
||||||
|
|
||||||
[[ports]]
|
|
||||||
localPort = 36697
|
|
||||||
externalPort = 4200
|
|
||||||
|
|
||||||
[[ports]]
|
[[ports]]
|
||||||
localPort = 39063
|
localPort = 39063
|
||||||
externalPort = 3001
|
externalPort = 3001
|
||||||
|
|||||||
+1
-1
@@ -348,7 +348,7 @@ function App() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
case 'settings':
|
case 'settings':
|
||||||
return <Settings />;
|
return <Settings onNavigateToTemplates={() => setCurrentTab('templates')} />;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -172,15 +172,31 @@
|
|||||||
"english": "Englisch (English)",
|
"english": "Englisch (English)",
|
||||||
"german": "Deutsch"
|
"german": "Deutsch"
|
||||||
},
|
},
|
||||||
"appearance": {
|
"labels": {
|
||||||
"title": "Erscheinungsbild",
|
"title": "Aufgabenlabels",
|
||||||
"description": "Passe das Aussehen an",
|
"description": "Erstellen und verwalten Sie Labels, um Ihre Aufgaben zu organisieren",
|
||||||
"themeToggle": "Thema-Umschalter ist in der Kopfzeile verfügbar"
|
"createLabel": "Label erstellen",
|
||||||
|
"createNewLabel": "Neues Label erstellen",
|
||||||
|
"editLabel": "Label bearbeiten",
|
||||||
|
"labelNamePlaceholder": "Label-Name",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"create": "Erstellen",
|
||||||
|
"update": "Aktualisieren",
|
||||||
|
"loading": "Labels werden geladen...",
|
||||||
|
"noLabels": "Noch keine Labels",
|
||||||
|
"noLabelsDescription": "Erstellen Sie Ihr erstes Label, um Ihre Aufgaben nach Farbe und Kategorie zu organisieren.",
|
||||||
|
"deleteConfirm": "Sind Sie sicher, dass Sie dieses Label löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"created": "Label erstellt",
|
||||||
|
"createdDescription": "Ihr Label wurde erfolgreich erstellt.",
|
||||||
|
"updated": "Label aktualisiert",
|
||||||
|
"updatedDescription": "Ihr Label wurde erfolgreich aktualisiert.",
|
||||||
|
"deleted": "Label gelöscht",
|
||||||
|
"deletedDescription": "Ihr Label wurde erfolgreich gelöscht."
|
||||||
},
|
},
|
||||||
"templates": {
|
"templates": {
|
||||||
"title": "Projektvorlagen",
|
"title": "Projektvorlagen",
|
||||||
"description": "Verwende Vorlagen zum schnellen Erstellen von Projekten",
|
"description": "Verwende Vorlagen zum schnellen Erstellen von Projekten",
|
||||||
"accessInfo": "Zugriff auf Vorlagen über das Navigationsmenü"
|
"manageTemplates": "Vorlagen verwalten"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"projectTemplate": {
|
"projectTemplate": {
|
||||||
|
|||||||
@@ -172,15 +172,31 @@
|
|||||||
"english": "English",
|
"english": "English",
|
||||||
"german": "German (Deutsch)"
|
"german": "German (Deutsch)"
|
||||||
},
|
},
|
||||||
"appearance": {
|
"labels": {
|
||||||
"title": "Appearance",
|
"title": "Task Labels",
|
||||||
"description": "Customize the look and feel",
|
"description": "Create and manage labels to organize your tasks",
|
||||||
"themeToggle": "Theme toggle available in the header"
|
"createLabel": "Create Label",
|
||||||
|
"createNewLabel": "Create New Label",
|
||||||
|
"editLabel": "Edit Label",
|
||||||
|
"labelNamePlaceholder": "Label name",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"create": "Create",
|
||||||
|
"update": "Update",
|
||||||
|
"loading": "Loading labels...",
|
||||||
|
"noLabels": "No labels yet",
|
||||||
|
"noLabelsDescription": "Create your first label to organize your tasks by color and category.",
|
||||||
|
"deleteConfirm": "Are you sure you want to delete this label? This action cannot be undone.",
|
||||||
|
"created": "Label created",
|
||||||
|
"createdDescription": "Your label has been created successfully.",
|
||||||
|
"updated": "Label updated",
|
||||||
|
"updatedDescription": "Your label has been updated successfully.",
|
||||||
|
"deleted": "Label deleted",
|
||||||
|
"deletedDescription": "Your label has been deleted successfully."
|
||||||
},
|
},
|
||||||
"templates": {
|
"templates": {
|
||||||
"title": "Project Templates",
|
"title": "Project Templates",
|
||||||
"description": "Use templates to quickly create projects",
|
"description": "Use templates to quickly create projects",
|
||||||
"accessInfo": "Access templates from the navigation menu"
|
"manageTemplates": "Manage Templates"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"projectTemplate": {
|
"projectTemplate": {
|
||||||
|
|||||||
+253
-16
@@ -1,10 +1,28 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
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 { 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) => {
|
const changeLanguage = (lng: string) => {
|
||||||
i18n.changeLanguage(lng);
|
i18n.changeLanguage(lng);
|
||||||
@@ -12,6 +30,83 @@ export default function Settings() {
|
|||||||
console.log('Language changed to:', lng);
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
@@ -48,33 +143,175 @@ export default function Settings() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Appearance Settings */}
|
{/* Labels Management */}
|
||||||
<Card>
|
<Card data-testid="card-labels-management">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t('settings.appearance.title')}</CardTitle>
|
<div className="flex items-center justify-between">
|
||||||
<CardDescription>
|
<div>
|
||||||
{t('settings.appearance.description')}
|
<CardTitle className="flex items-center gap-2">
|
||||||
</CardDescription>
|
<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>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-sm text-muted-foreground">
|
{labelsLoading ? (
|
||||||
{t('settings.appearance.themeToggle')}
|
<div className="flex items-center justify-center p-8">
|
||||||
</p>
|
<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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Project Templates */}
|
{/* Project Templates */}
|
||||||
<Card>
|
<Card data-testid="card-project-templates">
|
||||||
<CardHeader>
|
<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>
|
<CardDescription>
|
||||||
{t('settings.templates.description')}
|
{t('settings.templates.description')}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-sm text-muted-foreground">
|
<Button
|
||||||
{t('settings.templates.accessInfo')}
|
onClick={onNavigateToTemplates}
|
||||||
</p>
|
data-testid="button-manage-templates"
|
||||||
|
>
|
||||||
|
<Layers className="w-4 h-4 mr-2" />
|
||||||
|
{t('settings.templates.manageTemplates')}
|
||||||
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user