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 { 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 { useTasksContext } from '@/contexts/TasksContext';
import { useUserPreferences } from '@/contexts/UserPreferencesContext'; import { useUserPreferences } from '@/contexts/UserPreferencesContext';
import { useDraggable } from '@dnd-kit/core'; import { useDraggable } from '@dnd-kit/core';
@@ -14,6 +14,7 @@ interface TaskCardProps {
export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) { export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const { tags: availableTags, refreshTasks } = useTasksContext(); const { tags: availableTags, refreshTasks } = useTasksContext();
const { preferences } = useUserPreferences(); const { preferences } = useUserPreferences();
@@ -23,17 +24,19 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
id: task.id, id: task.id,
}); });
const handleDelete = async () => { const handleDelete = () => {
if (window.confirm('Êtes-vous sûr de vouloir supprimer cette tâche ?')) { setShowDeleteConfirm(true);
startTransition(async () => { };
const result = await deleteTask(task.id);
if (!result.success) { const confirmDelete = async () => {
console.error('Error deleting task:', result.error); startTransition(async () => {
} else { const result = await deleteTask(task.id);
await refreshTasks(); if (!result.success) {
} console.error('Error deleting task:', result.error);
}); } else {
} await refreshTasks();
}
});
}; };
const handleEdit = () => { const handleEdit = () => {
@@ -61,36 +64,50 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
: undefined; : undefined;
return ( return (
<UITaskCard <>
ref={setNodeRef} <UITaskCard
style={style} ref={setNodeRef}
variant={compactView ? 'compact' : 'detailed'} style={style}
source={task.source || 'manual'} variant={compactView ? 'compact' : 'detailed'}
title={task.title} source={task.source || 'manual'}
description={task.description} title={task.title}
tags={task.tags} description={task.description}
priority={task.priority} tags={task.tags}
status={task.status} priority={task.priority}
dueDate={task.dueDate} status={task.status}
completedAt={task.completedAt} dueDate={task.dueDate}
jiraKey={task.jiraKey} completedAt={task.completedAt}
jiraProject={task.jiraProject} jiraKey={task.jiraKey}
jiraType={task.jiraType} jiraProject={task.jiraProject}
tfsPullRequestId={task.tfsPullRequestId} jiraType={task.jiraType}
tfsProject={task.tfsProject} tfsPullRequestId={task.tfsPullRequestId}
tfsRepository={task.tfsRepository} tfsProject={task.tfsProject}
todosCount={task.todosCount} tfsRepository={task.tfsRepository}
isDragging={isDragging} todosCount={task.todosCount}
isPending={isPending} isDragging={isDragging}
onEdit={handleEdit} isPending={isPending}
onDelete={handleDelete} onEdit={handleEdit}
onTitleSave={handleTitleSave} onDelete={handleDelete}
fontSize={preferences.viewPreferences.fontSize} onTitleSave={handleTitleSave}
availableTags={availableTags} fontSize={preferences.viewPreferences.fontSize}
jiraConfig={preferences.jiraConfig} availableTags={availableTags}
tfsConfig={preferences.tfsConfig} jiraConfig={preferences.jiraConfig}
{...attributes} tfsConfig={preferences.tfsConfig}
{...listeners} {...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 { useState, useEffect } from 'react';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { ConfirmModal } from '@/components/ui/ConfirmModal';
import { useJiraConfig } from '@/hooks/useJiraConfig'; import { useJiraConfig } from '@/hooks/useJiraConfig';
import { jiraConfigClient } from '@/clients/jira-config-client'; import { jiraConfigClient } from '@/clients/jira-config-client';
@@ -21,6 +22,7 @@ export function JiraConfigForm() {
const [isValidating, setIsValidating] = useState(false); const [isValidating, setIsValidating] = useState(false);
const [validationResult, setValidationResult] = useState<{ type: 'success' | 'error', text: string } | null>(null); const [validationResult, setValidationResult] = useState<{ type: 'success' | 'error', text: string } | null>(null);
const [message, setMessage] = 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); const [showForm, setShowForm] = useState(false);
// Charger les données existantes dans le formulaire // Charger les données existantes dans le formulaire
@@ -77,10 +79,10 @@ export function JiraConfigForm() {
}; };
const handleDelete = async () => { const handleDelete = async () => {
if (!confirm('Êtes-vous sûr de vouloir supprimer la configuration Jira ?')) { setShowDeleteConfirm(true);
return; };
}
const confirmDelete = async () => {
setIsSubmitting(true); setIsSubmitting(true);
setMessage(null); setMessage(null);
@@ -444,6 +446,18 @@ export function JiraConfigForm() {
{message.text} {message.text}
</div> </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> </div>
); );
} }

View File

@@ -5,6 +5,7 @@ import { TfsConfig } from '@/services/integrations/tfs';
import { getTfsConfig, saveTfsConfig, deleteAllTfsTasks } from '@/actions/tfs'; import { getTfsConfig, saveTfsConfig, deleteAllTfsTasks } from '@/actions/tfs';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { ConfirmModal } from '@/components/ui/ConfirmModal';
export function TfsConfigForm() { export function TfsConfigForm() {
const [config, setConfig] = useState<TfsConfig>({ const [config, setConfig] = useState<TfsConfig>({
@@ -24,6 +25,8 @@ export function TfsConfigForm() {
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [deletingTasks, setDeletingTasks] = useState(false); const [deletingTasks, setDeletingTasks] = useState(false);
const [showDeleteConfigConfirm, setShowDeleteConfigConfirm] = useState(false);
const [showDeleteTasksConfirm, setShowDeleteTasksConfirm] = useState(false);
// Charger la configuration existante // Charger la configuration existante
useEffect(() => { useEffect(() => {
@@ -85,10 +88,10 @@ export function TfsConfigForm() {
}; };
const handleDelete = async () => { const handleDelete = async () => {
if (!confirm('Êtes-vous sûr de vouloir supprimer la configuration TFS ?')) { setShowDeleteConfigConfirm(true);
return; };
}
const confirmDeleteConfig = async () => {
startTransition(async () => { startTransition(async () => {
setMessage(null); setMessage(null);
// Réinitialiser la config // Réinitialiser la config
@@ -171,17 +174,10 @@ export function TfsConfigForm() {
}; };
const handleDeleteAllTasks = async () => { const handleDeleteAllTasks = async () => {
const confirmation = confirm( setShowDeleteTasksConfirm(true);
'Ê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;
}
const confirmDeleteAllTasks = async () => {
try { try {
setDeletingTasks(true); setDeletingTasks(true);
setMessage(null); setMessage(null);
@@ -634,6 +630,30 @@ export function TfsConfigForm() {
{message.text} {message.text}
</div> </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> </div>
); );
} }

View File

@@ -4,6 +4,7 @@ import { useState, useMemo } from 'react';
import { Tag } from '@/lib/types'; import { Tag } from '@/lib/types';
import { Card, CardContent, CardHeader } from '@/components/ui/Card'; import { Card, CardContent, CardHeader } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { ConfirmModal } from '@/components/ui/ConfirmModal';
import { TagForm } from '@/components/forms/TagForm'; import { TagForm } from '@/components/forms/TagForm';
import { TagsStats } from './TagsStats'; import { TagsStats } from './TagsStats';
import { TagsFilters } from './TagsFilters'; import { TagsFilters } from './TagsFilters';
@@ -22,6 +23,8 @@ export function TagsManagement({ tags, onRefreshTags, onDeleteTag }: TagsManagem
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [editingTag, setEditingTag] = useState<Tag | null>(null); const [editingTag, setEditingTag] = useState<Tag | null>(null);
const [deletingTagId, setDeletingTagId] = useState<string | 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 // Filtrer et trier les tags
const filteredTags = useMemo(() => { const filteredTags = useMemo(() => {
@@ -65,18 +68,22 @@ export function TagsManagement({ tags, onRefreshTags, onDeleteTag }: TagsManagem
}; };
const handleDeleteTag = async (tag: Tag) => { const handleDeleteTag = async (tag: Tag) => {
if (!confirm(`Êtes-vous sûr de vouloir supprimer le tag "${tag.name}" ?`)) { setTagToDelete(tag);
return; setShowDeleteConfirm(true);
} };
setDeletingTagId(tag.id); const confirmDeleteTag = async () => {
if (!tagToDelete) return;
setDeletingTagId(tagToDelete.id);
try { try {
await onDeleteTag(tag.id); await onDeleteTag(tagToDelete.id);
await onRefreshTags(); await onRefreshTags();
} catch (error) { } catch (error) {
console.error('Erreur lors de la suppression:', error); console.error('Erreur lors de la suppression:', error);
} finally { } finally {
setDeletingTagId(null); 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 { ProgressBar } from '@/components/ui/ProgressBar';
import { EmptyState } from '@/components/ui/EmptyState'; import { EmptyState } from '@/components/ui/EmptyState';
import { DropZone } from '@/components/ui/DropZone'; 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() { 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[] = [ const alertItems: AlertItem[] = [
{ id: '1', title: 'Tâche critique', icon: '🔴', urgency: 'critical', metadata: 'Dans 1 jour' }, { 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: '2', title: 'Réunion urgente', icon: '🟠', urgency: 'high', metadata: 'Dans 2 jours' },
{ id: '3', title: 'Rappel', icon: '🟡', urgency: 'medium', metadata: 'Dans 5 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 ( return (
<section id="feedback" className="space-y-8"> <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"> <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>
</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 */} {/* Drop Zone */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-lg font-medium text-[var(--foreground)]">Drop Zone</h3> <h3 className="text-lg font-medium text-[var(--foreground)]">Drop Zone</h3>
@@ -183,6 +239,49 @@ export function FeedbackSection() {
</DropZone> </DropZone>
</div> </div>
</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> </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 // Composants existants
export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card'; export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card';
export { FontSizeToggle } from './FontSizeToggle'; export { FontSizeToggle } from './FontSizeToggle';
export { Modal } from './Modal';
export { ConfirmModal } from './ConfirmModal';