mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-21 14:08:15 +02:00
- Routes en React.lazy (sauf connexion et accueil) avec un Suspense dans la zone de contenu ; recharts n'est plus préchargé. JS initial : 1,72 Mo -> 625 Ko (183 Ko gzip). Rechargement automatique unique si un ancien fichier JS a disparu après un déploiement, et ErrorBoundary par page pour garder le menu affiché en cas d'erreur. - Authentification unifiée sur le cache React Query ['/api/user'] : un seul GET /api/user pour toute l'application (au lieu de 4 à 5 par page), plus de rechargement complet après la connexion. - Pas de nouvel essai sur les erreurs 4xx ; fournisseurs et magasins en cache 5 minutes ; valeur du StoreContext mémoïsée et redimensionnement qui ne re-rend que lorsque le palier d'écran change. - Pages : requêtes inutilisées ou en double supprimées, appels lancés seulement une fois l'utilisateur connu, invalidations ciblées au lieu de rechargements complets, filtres et tris mémoïsés, anciennes données gardées (et estompées) pendant un changement de filtre ou de mois, recherche DLC mobile temporisée, vérifications de factures à concurrence bornée, navigation interne sans rechargement de la page. - index.html : script de bannière Replit retiré, lang="fr". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MsDJjQrAggcJwbbBtKhgyb
650 lines
23 KiB
TypeScript
650 lines
23 KiB
TypeScript
import { useState } from "react";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { apiRequest } from "@/lib/queryClient";
|
|
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { z } from "zod";
|
|
import {
|
|
Database,
|
|
Plus,
|
|
Edit,
|
|
Trash2,
|
|
Settings,
|
|
Eye,
|
|
EyeOff,
|
|
CheckCircle,
|
|
XCircle,
|
|
AlertTriangle,
|
|
Link,
|
|
} from "lucide-react";
|
|
import type { NocodbConfig } from "@shared/schema";
|
|
|
|
// Form schemas
|
|
const nocodbConfigFormSchema = z.object({
|
|
name: z.string().min(1, "Le nom est obligatoire"),
|
|
baseUrl: z.string().url("L'URL doit être valide").min(1, "L'URL de base est obligatoire"),
|
|
apiToken: z.string().min(1, "Le token API est obligatoire"),
|
|
projectId: z.string().min(1, "L'ID du projet est obligatoire"),
|
|
description: z.string().optional(),
|
|
isActive: z.boolean().default(true),
|
|
});
|
|
|
|
type NocodbConfigForm = z.infer<typeof nocodbConfigFormSchema>;
|
|
|
|
export default function NocoDBConfig() {
|
|
const { user } = useAuthUnified();
|
|
const { toast } = useToast();
|
|
const queryClient = useQueryClient();
|
|
|
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
const [showEditModal, setShowEditModal] = useState(false);
|
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
|
const [selectedConfig, setSelectedConfig] = useState<NocodbConfig | null>(null);
|
|
const [showTokens, setShowTokens] = useState<{ [key: number]: boolean }>({});
|
|
|
|
// Queries avec protection complète
|
|
const { data: rawConfigs, isLoading } = useQuery({
|
|
queryKey: ['/api/nocodb-config'],
|
|
enabled: user?.role === 'admin',
|
|
});
|
|
|
|
// Protection quadruple couche pour éviter les erreurs TypeError
|
|
const configs = rawConfigs || [];
|
|
const safeConfigs = Array.isArray(configs) ? configs : [];
|
|
|
|
// Mutations
|
|
const createConfigMutation = useMutation({
|
|
mutationFn: (data: NocodbConfigForm) =>
|
|
apiRequest('/api/nocodb-config', 'POST', data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['/api/nocodb-config'] });
|
|
setShowCreateModal(false);
|
|
toast({
|
|
title: "Configuration créée",
|
|
description: "La configuration NocoDB a été créée avec succès.",
|
|
});
|
|
},
|
|
onError: (error: any) => {
|
|
toast({
|
|
title: "Erreur",
|
|
description: error.message || "Erreur lors de la création de la configuration.",
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
const updateConfigMutation = useMutation({
|
|
mutationFn: ({ id, data }: { id: number; data: Partial<NocodbConfigForm> }) =>
|
|
apiRequest(`/api/nocodb-config/${id}`, 'PUT', data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['/api/nocodb-config'] });
|
|
setShowEditModal(false);
|
|
setSelectedConfig(null);
|
|
toast({
|
|
title: "Configuration mise à jour",
|
|
description: "La configuration NocoDB a été mise à jour avec succès.",
|
|
});
|
|
},
|
|
onError: (error: any) => {
|
|
toast({
|
|
title: "Erreur",
|
|
description: error.message || "Erreur lors de la mise à jour de la configuration.",
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
const deleteConfigMutation = useMutation({
|
|
mutationFn: (id: number) =>
|
|
apiRequest(`/api/nocodb-config/${id}`, 'DELETE'),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['/api/nocodb-config'] });
|
|
setShowDeleteModal(false);
|
|
setSelectedConfig(null);
|
|
toast({
|
|
title: "Configuration supprimée",
|
|
description: "La configuration NocoDB a été supprimée avec succès.",
|
|
});
|
|
},
|
|
onError: (error: any) => {
|
|
toast({
|
|
title: "Erreur",
|
|
description: error.message || "Erreur lors de la suppression de la configuration.",
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
// Forms
|
|
const createForm = useForm<NocodbConfigForm>({
|
|
resolver: zodResolver(nocodbConfigFormSchema),
|
|
defaultValues: {
|
|
name: "",
|
|
baseUrl: "",
|
|
apiToken: "",
|
|
projectId: "",
|
|
description: "",
|
|
isActive: true,
|
|
},
|
|
});
|
|
|
|
const editForm = useForm<NocodbConfigForm>({
|
|
resolver: zodResolver(nocodbConfigFormSchema),
|
|
});
|
|
|
|
// Handle create config
|
|
const handleCreateConfig = (data: NocodbConfigForm) => {
|
|
createConfigMutation.mutate(data);
|
|
};
|
|
|
|
// Handle edit config
|
|
const handleEditConfig = (data: NocodbConfigForm) => {
|
|
if (!selectedConfig) return;
|
|
updateConfigMutation.mutate({ id: selectedConfig.id, data });
|
|
};
|
|
|
|
// Handle delete config
|
|
const handleDeleteConfig = () => {
|
|
if (!selectedConfig) return;
|
|
deleteConfigMutation.mutate(selectedConfig.id);
|
|
};
|
|
|
|
// Open edit modal
|
|
const openEditModal = (config: NocodbConfig) => {
|
|
setSelectedConfig(config);
|
|
editForm.reset({
|
|
name: config.name,
|
|
baseUrl: config.baseUrl,
|
|
apiToken: config.apiToken,
|
|
projectId: config.projectId,
|
|
description: config.description || "",
|
|
isActive: config.isActive ?? undefined,
|
|
});
|
|
setShowEditModal(true);
|
|
};
|
|
|
|
// Open delete modal
|
|
const openDeleteModal = (config: NocodbConfig) => {
|
|
setSelectedConfig(config);
|
|
setShowDeleteModal(true);
|
|
};
|
|
|
|
// Toggle token visibility
|
|
const toggleTokenVisibility = (configId: number) => {
|
|
setShowTokens(prev => ({
|
|
...prev,
|
|
[configId]: !prev[configId]
|
|
}));
|
|
};
|
|
|
|
// Format token for display
|
|
const formatToken = (token: string, isVisible: boolean) => {
|
|
if (isVisible) return token;
|
|
return '•'.repeat(Math.min(token.length, 20));
|
|
};
|
|
|
|
if (user?.role !== 'admin') {
|
|
return (
|
|
<div className="container mx-auto px-4 py-8">
|
|
<Card>
|
|
<CardContent className="p-6 text-center">
|
|
<AlertTriangle className="h-12 w-12 text-yellow-500 mx-auto mb-4" />
|
|
<h3 className="text-lg font-semibold mb-2">Accès restreint</h3>
|
|
<p className="text-muted-foreground">
|
|
Seuls les administrateurs peuvent gérer les configurations NocoDB.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="container mx-auto px-4 py-8">
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary"></div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-8">
|
|
{/* Header */}
|
|
<div className="flex justify-between items-center mb-8">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-gray-900 flex items-center gap-2">
|
|
<Database className="h-8 w-8 text-blue-600" />
|
|
Configuration NocoDB
|
|
</h1>
|
|
<p className="text-muted-foreground mt-2">
|
|
Gérez les connexions aux bases de données NocoDB externes pour récupérer des informations.
|
|
</p>
|
|
</div>
|
|
<Button onClick={() => setShowCreateModal(true)} className="gap-2">
|
|
<Plus className="h-4 w-4" />
|
|
Nouvelle Configuration
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Configurations List */}
|
|
<div className="grid gap-6">
|
|
{safeConfigs.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="p-8 text-center">
|
|
<Database className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
|
<h3 className="text-lg font-semibold mb-2">Aucune configuration</h3>
|
|
<p className="text-muted-foreground mb-4">
|
|
Commencez par créer votre première configuration NocoDB.
|
|
</p>
|
|
<Button onClick={() => setShowCreateModal(true)} className="gap-2">
|
|
<Plus className="h-4 w-4" />
|
|
Créer une configuration
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
safeConfigs.map((config: NocodbConfig) => (
|
|
<Card key={config.id} className="border shadow-sm">
|
|
<CardHeader className="pb-4">
|
|
<div className="flex justify-between items-start">
|
|
<div className="flex items-start gap-3">
|
|
<div className="p-2 bg-blue-100 rounded-lg">
|
|
<Database className="h-5 w-5 text-blue-600" />
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<CardTitle className="text-lg">{config.name}</CardTitle>
|
|
<Badge variant={config.isActive ? "default" : "secondary"}>
|
|
{config.isActive ? (
|
|
<><CheckCircle className="h-3 w-3 mr-1" /> Actif</>
|
|
) : (
|
|
<><XCircle className="h-3 w-3 mr-1" /> Inactif</>
|
|
)}
|
|
</Badge>
|
|
</div>
|
|
{config.description && (
|
|
<CardDescription className="mt-1">
|
|
{config.description}
|
|
</CardDescription>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => openEditModal(config)}
|
|
>
|
|
<Edit className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => openDeleteModal(config)}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-3">
|
|
<div>
|
|
<label className="text-sm font-medium text-gray-600">URL de base</label>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<Link className="h-4 w-4 text-gray-400" />
|
|
<span className="text-sm font-mono bg-gray-100 px-2 py-1 rounded">
|
|
{config.baseUrl}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-gray-600">ID du projet</label>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<span className="text-sm font-mono bg-gray-100 px-2 py-1 rounded">
|
|
{config.projectId}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-gray-600">Token API</label>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<span className="text-sm font-mono bg-gray-100 px-2 py-1 rounded flex-1">
|
|
{formatToken(config.apiToken, showTokens[config.id])}
|
|
</span>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => toggleTokenVisibility(config.id)}
|
|
>
|
|
{showTokens[config.id] ? (
|
|
<EyeOff className="h-4 w-4" />
|
|
) : (
|
|
<Eye className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
{/* Create Modal */}
|
|
<Dialog open={showCreateModal} onOpenChange={setShowCreateModal}>
|
|
<DialogContent className="sm:max-w-md" aria-describedby="create-config-description">
|
|
<DialogHeader>
|
|
<DialogTitle>Nouvelle Configuration NocoDB</DialogTitle>
|
|
</DialogHeader>
|
|
<div id="create-config-description" className="sr-only">
|
|
Créer une nouvelle configuration pour se connecter à une base de données NocoDB externe
|
|
</div>
|
|
<Form {...createForm}>
|
|
<form onSubmit={createForm.handleSubmit(handleCreateConfig)} className="space-y-4">
|
|
<FormField
|
|
control={createForm.control}
|
|
name="name"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Nom de la configuration</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="Ex: Production NocoDB" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={createForm.control}
|
|
name="baseUrl"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>URL de base</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="https://your-nocodb-instance.com" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={createForm.control}
|
|
name="apiToken"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Personal API Token</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type="password"
|
|
placeholder="xc-token-xxxxxxxxxxxxxx"
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={createForm.control}
|
|
name="projectId"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>ID du projet</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
placeholder="p_xxxxxxxxxxxxxx"
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={createForm.control}
|
|
name="description"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Description (optionnel)</FormLabel>
|
|
<FormControl>
|
|
<Textarea
|
|
placeholder="Description de cette configuration..."
|
|
rows={3}
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={createForm.control}
|
|
name="isActive"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
<div className="space-y-0.5">
|
|
<FormLabel className="text-base">Configuration active</FormLabel>
|
|
<div className="text-sm text-muted-foreground">
|
|
Activer cette configuration pour l'utiliser
|
|
</div>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<div className="flex justify-end space-x-3 pt-4">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => setShowCreateModal(false)}
|
|
disabled={createConfigMutation.isPending}
|
|
>
|
|
Annuler
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
disabled={createConfigMutation.isPending}
|
|
>
|
|
{createConfigMutation.isPending ? "Création..." : "Créer"}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Edit Modal */}
|
|
<Dialog open={showEditModal} onOpenChange={setShowEditModal}>
|
|
<DialogContent className="sm:max-w-md" aria-describedby="edit-config-description">
|
|
<DialogHeader>
|
|
<DialogTitle>Modifier la Configuration</DialogTitle>
|
|
</DialogHeader>
|
|
<div id="edit-config-description" className="sr-only">
|
|
Modifier les paramètres de la configuration NocoDB sélectionnée
|
|
</div>
|
|
<Form {...editForm}>
|
|
<form onSubmit={editForm.handleSubmit(handleEditConfig)} className="space-y-4">
|
|
<FormField
|
|
control={editForm.control}
|
|
name="name"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Nom de la configuration</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="Ex: Production NocoDB" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={editForm.control}
|
|
name="baseUrl"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>URL de base</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="https://your-nocodb-instance.com" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={editForm.control}
|
|
name="apiToken"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Personal API Token</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type="password"
|
|
placeholder="xc-token-xxxxxxxxxxxxxx"
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={editForm.control}
|
|
name="projectId"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>ID du projet</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
placeholder="p_xxxxxxxxxxxxxx"
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={editForm.control}
|
|
name="description"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Description (optionnel)</FormLabel>
|
|
<FormControl>
|
|
<Textarea
|
|
placeholder="Description de cette configuration..."
|
|
rows={3}
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={editForm.control}
|
|
name="isActive"
|
|
render={({ field }) => (
|
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
<div className="space-y-0.5">
|
|
<FormLabel className="text-base">Configuration active</FormLabel>
|
|
<div className="text-sm text-muted-foreground">
|
|
Activer cette configuration pour l'utiliser
|
|
</div>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<div className="flex justify-end space-x-3 pt-4">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => setShowEditModal(false)}
|
|
disabled={updateConfigMutation.isPending}
|
|
>
|
|
Annuler
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
disabled={updateConfigMutation.isPending}
|
|
>
|
|
{updateConfigMutation.isPending ? "Mise à jour..." : "Mettre à jour"}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Delete Modal */}
|
|
<Dialog open={showDeleteModal} onOpenChange={setShowDeleteModal}>
|
|
<DialogContent className="sm:max-w-md" aria-describedby="delete-config-description">
|
|
<DialogHeader>
|
|
<DialogTitle>Confirmer la suppression</DialogTitle>
|
|
</DialogHeader>
|
|
<div id="delete-config-description" className="sr-only">
|
|
Confirmer la suppression définitive de la configuration NocoDB
|
|
</div>
|
|
<div className="py-4">
|
|
<p className="text-sm text-muted-foreground">
|
|
Êtes-vous sûr de vouloir supprimer la configuration "{selectedConfig?.name}" ?
|
|
Cette action est irréversible.
|
|
</p>
|
|
</div>
|
|
<div className="flex justify-end space-x-3">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowDeleteModal(false)}
|
|
disabled={deleteConfigMutation.isPending}
|
|
>
|
|
Annuler
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
onClick={handleDeleteConfig}
|
|
disabled={deleteConfigMutation.isPending}
|
|
>
|
|
{deleteConfigMutation.isPending ? "Suppression..." : "Supprimer"}
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
} |