feat: integrate ConfirmModal for delete confirmations across components

- Added `ConfirmModal` to `TaskCard`, `JiraConfigForm`, `TfsConfigForm`, and `TagsManagement` for improved user experience during delete actions.
- Replaced direct confirmation prompts with modals, enhancing UI consistency and usability.
- Updated state management to handle modal visibility and confirmation logic effectively.
This commit is contained in:
Julien Froidefond
2025-10-01 13:47:57 +02:00
parent 352a65af47
commit f13ed5b8d9
7 changed files with 299 additions and 65 deletions

View File

@@ -1,6 +1,6 @@
import { useTransition } from 'react';
import { useTransition, useState } from 'react';
import { Task } from '@/lib/types';
import { TaskCard as UITaskCard } from '@/components/ui/TaskCard';
import { TaskCard as UITaskCard, ConfirmModal } from '@/components/ui';
import { useTasksContext } from '@/contexts/TasksContext';
import { useUserPreferences } from '@/contexts/UserPreferencesContext';
import { useDraggable } from '@dnd-kit/core';
@@ -14,6 +14,7 @@ interface TaskCardProps {
export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
const [isPending, startTransition] = useTransition();
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const { tags: availableTags, refreshTasks } = useTasksContext();
const { preferences } = useUserPreferences();
@@ -23,17 +24,19 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
id: task.id,
});
const handleDelete = async () => {
if (window.confirm('Êtes-vous sûr de vouloir supprimer cette tâche ?')) {
startTransition(async () => {
const result = await deleteTask(task.id);
if (!result.success) {
console.error('Error deleting task:', result.error);
} else {
await refreshTasks();
}
});
}
const handleDelete = () => {
setShowDeleteConfirm(true);
};
const confirmDelete = async () => {
startTransition(async () => {
const result = await deleteTask(task.id);
if (!result.success) {
console.error('Error deleting task:', result.error);
} else {
await refreshTasks();
}
});
};
const handleEdit = () => {
@@ -61,36 +64,50 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
: undefined;
return (
<UITaskCard
ref={setNodeRef}
style={style}
variant={compactView ? 'compact' : 'detailed'}
source={task.source || 'manual'}
title={task.title}
description={task.description}
tags={task.tags}
priority={task.priority}
status={task.status}
dueDate={task.dueDate}
completedAt={task.completedAt}
jiraKey={task.jiraKey}
jiraProject={task.jiraProject}
jiraType={task.jiraType}
tfsPullRequestId={task.tfsPullRequestId}
tfsProject={task.tfsProject}
tfsRepository={task.tfsRepository}
todosCount={task.todosCount}
isDragging={isDragging}
isPending={isPending}
onEdit={handleEdit}
onDelete={handleDelete}
onTitleSave={handleTitleSave}
fontSize={preferences.viewPreferences.fontSize}
availableTags={availableTags}
jiraConfig={preferences.jiraConfig}
tfsConfig={preferences.tfsConfig}
{...attributes}
{...listeners}
/>
<>
<UITaskCard
ref={setNodeRef}
style={style}
variant={compactView ? 'compact' : 'detailed'}
source={task.source || 'manual'}
title={task.title}
description={task.description}
tags={task.tags}
priority={task.priority}
status={task.status}
dueDate={task.dueDate}
completedAt={task.completedAt}
jiraKey={task.jiraKey}
jiraProject={task.jiraProject}
jiraType={task.jiraType}
tfsPullRequestId={task.tfsPullRequestId}
tfsProject={task.tfsProject}
tfsRepository={task.tfsRepository}
todosCount={task.todosCount}
isDragging={isDragging}
isPending={isPending}
onEdit={handleEdit}
onDelete={handleDelete}
onTitleSave={handleTitleSave}
fontSize={preferences.viewPreferences.fontSize}
availableTags={availableTags}
jiraConfig={preferences.jiraConfig}
tfsConfig={preferences.tfsConfig}
{...attributes}
{...listeners}
/>
<ConfirmModal
isOpen={showDeleteConfirm}
onClose={() => setShowDeleteConfirm(false)}
onConfirm={confirmDelete}
title="Supprimer la tâche"
message="Êtes-vous sûr de vouloir supprimer cette tâche ? Cette action est irréversible."
confirmText="Supprimer"
cancelText="Annuler"
variant="destructive"
isLoading={isPending}
/>
</>
);
}

View File

@@ -3,6 +3,7 @@
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { ConfirmModal } from '@/components/ui/ConfirmModal';
import { useJiraConfig } from '@/hooks/useJiraConfig';
import { jiraConfigClient } from '@/clients/jira-config-client';
@@ -21,6 +22,7 @@ export function JiraConfigForm() {
const [isValidating, setIsValidating] = useState(false);
const [validationResult, setValidationResult] = useState<{ type: 'success' | 'error', text: string } | null>(null);
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [showForm, setShowForm] = useState(false);
// Charger les données existantes dans le formulaire
@@ -77,10 +79,10 @@ export function JiraConfigForm() {
};
const handleDelete = async () => {
if (!confirm('Êtes-vous sûr de vouloir supprimer la configuration Jira ?')) {
return;
}
setShowDeleteConfirm(true);
};
const confirmDelete = async () => {
setIsSubmitting(true);
setMessage(null);
@@ -444,6 +446,18 @@ export function JiraConfigForm() {
{message.text}
</div>
)}
<ConfirmModal
isOpen={showDeleteConfirm}
onClose={() => setShowDeleteConfirm(false)}
onConfirm={confirmDelete}
title="Supprimer la configuration Jira"
message="Êtes-vous sûr de vouloir supprimer la configuration Jira ?"
confirmText="Supprimer"
cancelText="Annuler"
variant="destructive"
isLoading={isSubmitting}
/>
</div>
);
}

View File

@@ -5,6 +5,7 @@ import { TfsConfig } from '@/services/integrations/tfs';
import { getTfsConfig, saveTfsConfig, deleteAllTfsTasks } from '@/actions/tfs';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { ConfirmModal } from '@/components/ui/ConfirmModal';
export function TfsConfigForm() {
const [config, setConfig] = useState<TfsConfig>({
@@ -24,6 +25,8 @@ export function TfsConfigForm() {
const [showForm, setShowForm] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [deletingTasks, setDeletingTasks] = useState(false);
const [showDeleteConfigConfirm, setShowDeleteConfigConfirm] = useState(false);
const [showDeleteTasksConfirm, setShowDeleteTasksConfirm] = useState(false);
// Charger la configuration existante
useEffect(() => {
@@ -85,10 +88,10 @@ export function TfsConfigForm() {
};
const handleDelete = async () => {
if (!confirm('Êtes-vous sûr de vouloir supprimer la configuration TFS ?')) {
return;
}
setShowDeleteConfigConfirm(true);
};
const confirmDeleteConfig = async () => {
startTransition(async () => {
setMessage(null);
// Réinitialiser la config
@@ -171,17 +174,10 @@ export function TfsConfigForm() {
};
const handleDeleteAllTasks = async () => {
const confirmation = confirm(
'Êtes-vous sûr de vouloir supprimer TOUTES les tâches TFS de la base locale ?\n\n' +
'Cette action est irréversible et supprimera définitivement toutes les tâches ' +
'synchronisées depuis Azure DevOps/TFS.\n\n' +
'Cliquez sur OK pour confirmer la suppression.'
);
if (!confirmation) {
return;
}
setShowDeleteTasksConfirm(true);
};
const confirmDeleteAllTasks = async () => {
try {
setDeletingTasks(true);
setMessage(null);
@@ -634,6 +630,30 @@ export function TfsConfigForm() {
{message.text}
</div>
)}
<ConfirmModal
isOpen={showDeleteConfigConfirm}
onClose={() => setShowDeleteConfigConfirm(false)}
onConfirm={confirmDeleteConfig}
title="Supprimer la configuration TFS"
message="Êtes-vous sûr de vouloir supprimer la configuration TFS ?"
confirmText="Supprimer"
cancelText="Annuler"
variant="destructive"
isLoading={isPending}
/>
<ConfirmModal
isOpen={showDeleteTasksConfirm}
onClose={() => setShowDeleteTasksConfirm(false)}
onConfirm={confirmDeleteAllTasks}
title="Supprimer toutes les tâches TFS"
message="Êtes-vous sûr de vouloir supprimer TOUTES les tâches TFS de la base locale ? Cette action est irréversible et supprimera définitivement toutes les tâches synchronisées depuis Azure DevOps/TFS."
confirmText="Supprimer définitivement"
cancelText="Annuler"
variant="destructive"
isLoading={deletingTasks}
/>
</div>
);
}

View File

@@ -4,6 +4,7 @@ import { useState, useMemo } from 'react';
import { Tag } from '@/lib/types';
import { Card, CardContent, CardHeader } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { ConfirmModal } from '@/components/ui/ConfirmModal';
import { TagForm } from '@/components/forms/TagForm';
import { TagsStats } from './TagsStats';
import { TagsFilters } from './TagsFilters';
@@ -22,6 +23,8 @@ export function TagsManagement({ tags, onRefreshTags, onDeleteTag }: TagsManagem
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [editingTag, setEditingTag] = useState<Tag | null>(null);
const [deletingTagId, setDeletingTagId] = useState<string | null>(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [tagToDelete, setTagToDelete] = useState<Tag | null>(null);
// Filtrer et trier les tags
const filteredTags = useMemo(() => {
@@ -65,18 +68,22 @@ export function TagsManagement({ tags, onRefreshTags, onDeleteTag }: TagsManagem
};
const handleDeleteTag = async (tag: Tag) => {
if (!confirm(`Êtes-vous sûr de vouloir supprimer le tag "${tag.name}" ?`)) {
return;
}
setTagToDelete(tag);
setShowDeleteConfirm(true);
};
const confirmDeleteTag = async () => {
if (!tagToDelete) return;
setDeletingTagId(tag.id);
setDeletingTagId(tagToDelete.id);
try {
await onDeleteTag(tag.id);
await onDeleteTag(tagToDelete.id);
await onRefreshTags();
} catch (error) {
console.error('Erreur lors de la suppression:', error);
} finally {
setDeletingTagId(null);
setTagToDelete(null);
}
};
@@ -164,6 +171,21 @@ export function TagsManagement({ tags, onRefreshTags, onDeleteTag }: TagsManagem
}}
/>
)}
<ConfirmModal
isOpen={showDeleteConfirm}
onClose={() => {
setShowDeleteConfirm(false);
setTagToDelete(null);
}}
onConfirm={confirmDeleteTag}
title="Supprimer le tag"
message={`Êtes-vous sûr de vouloir supprimer le tag "${tagToDelete?.name}" ?`}
confirmText="Supprimer"
cancelText="Annuler"
variant="destructive"
isLoading={!!deletingTagId}
/>
</>
);
}

View File

@@ -6,14 +6,39 @@ import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { ProgressBar } from '@/components/ui/ProgressBar';
import { EmptyState } from '@/components/ui/EmptyState';
import { DropZone } from '@/components/ui/DropZone';
import { Modal } from '@/components/ui/Modal';
import { ConfirmModal } from '@/components/ui/ConfirmModal';
import { Button } from '@/components/ui/Button';
import { useState } from 'react';
export function FeedbackSection() {
const [showModal, setShowModal] = useState(false);
const [showConfirmModal, setShowConfirmModal] = useState(false);
const [showDestructiveConfirm, setShowDestructiveConfirm] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const alertItems: AlertItem[] = [
{ id: '1', title: 'Tâche critique', icon: '🔴', urgency: 'critical', metadata: 'Dans 1 jour' },
{ id: '2', title: 'Réunion urgente', icon: '🟠', urgency: 'high', metadata: 'Dans 2 jours' },
{ id: '3', title: 'Rappel', icon: '🟡', urgency: 'medium', metadata: 'Dans 5 jours' }
];
const handleConfirm = () => {
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
setShowConfirmModal(false);
}, 2000);
};
const handleDestructiveConfirm = () => {
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
setShowDestructiveConfirm(false);
}, 2000);
};
return (
<section id="feedback" className="space-y-8">
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
@@ -169,6 +194,37 @@ export function FeedbackSection() {
</div>
</div>
{/* Modals */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">Modals</h3>
<div className="space-y-4">
<div>
<p className="text-sm text-[var(--muted-foreground)] mb-3">Modal de base</p>
<Button onClick={() => setShowModal(true)}>
Ouvrir Modal
</Button>
</div>
<div>
<p className="text-sm text-[var(--muted-foreground)] mb-3">Modal de confirmation</p>
<div className="flex gap-2">
<Button
variant="primary"
onClick={() => setShowConfirmModal(true)}
>
Confirmation Standard
</Button>
<Button
variant="destructive"
onClick={() => setShowDestructiveConfirm(true)}
>
Confirmation Destructive
</Button>
</div>
</div>
</div>
</div>
{/* Drop Zone */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">Drop Zone</h3>
@@ -183,6 +239,49 @@ export function FeedbackSection() {
</DropZone>
</div>
</div>
{/* Modals */}
<Modal
isOpen={showModal}
onClose={() => setShowModal(false)}
title="Modal de démonstration"
size="md"
>
<div className="space-y-4">
<p className="text-[var(--foreground)]">
Ceci est un exemple de modal de base. Vous pouvez y mettre n&apos;importe quel contenu.
</p>
<div className="flex justify-end">
<Button onClick={() => setShowModal(false)}>
Fermer
</Button>
</div>
</div>
</Modal>
<ConfirmModal
isOpen={showConfirmModal}
onClose={() => setShowConfirmModal(false)}
onConfirm={handleConfirm}
title="Confirmer l'action"
message="Êtes-vous sûr de vouloir effectuer cette action ?"
confirmText="Confirmer"
cancelText="Annuler"
variant="primary"
isLoading={isLoading}
/>
<ConfirmModal
isOpen={showDestructiveConfirm}
onClose={() => setShowDestructiveConfirm(false)}
onConfirm={handleDestructiveConfirm}
title="Supprimer définitivement"
message="Cette action est irréversible. Êtes-vous sûr de vouloir continuer ?"
confirmText="Supprimer"
cancelText="Annuler"
variant="destructive"
isLoading={isLoading}
/>
</section>
);
}

View File

@@ -0,0 +1,60 @@
'use client';
import { Modal } from './Modal';
import { Button } from './Button';
interface ConfirmModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title?: string;
message: string;
confirmText?: string;
cancelText?: string;
variant?: 'primary' | 'destructive';
isLoading?: boolean;
}
export function ConfirmModal({
isOpen,
onClose,
onConfirm,
title = 'Confirmation',
message,
confirmText = 'Confirmer',
cancelText = 'Annuler',
variant = 'primary',
isLoading = false
}: ConfirmModalProps) {
const handleConfirm = () => {
onConfirm();
onClose();
};
return (
<Modal isOpen={isOpen} onClose={onClose} title={title} size="sm">
<div className="space-y-4">
<p className="text-[var(--foreground)] text-sm leading-relaxed">
{message}
</p>
<div className="flex justify-end gap-2">
<Button
variant="secondary"
onClick={onClose}
disabled={isLoading}
>
{cancelText}
</Button>
<Button
variant={variant}
onClick={handleConfirm}
disabled={isLoading}
>
{isLoading ? 'Chargement...' : confirmText}
</Button>
</div>
</div>
</Modal>
);
}

View File

@@ -45,3 +45,5 @@ export { MetricsGrid } from './MetricsGrid';
// Composants existants
export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card';
export { FontSizeToggle } from './FontSizeToggle';
export { Modal } from './Modal';
export { ConfirmModal } from './ConfirmModal';