feat: TFS Sync
This commit is contained in:
638
src/components/settings/TfsConfigForm.tsx
Normal file
638
src/components/settings/TfsConfigForm.tsx
Normal file
@@ -0,0 +1,638 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useTransition } from 'react';
|
||||
import { TfsConfig } from '@/services/tfs';
|
||||
import { getTfsConfig, saveTfsConfig, deleteAllTfsTasks } from '@/actions/tfs';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
|
||||
export function TfsConfigForm() {
|
||||
const [config, setConfig] = useState<TfsConfig>({
|
||||
enabled: false,
|
||||
organizationUrl: '',
|
||||
projectName: '',
|
||||
personalAccessToken: '',
|
||||
repositories: [],
|
||||
ignoredRepositories: [],
|
||||
});
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [message, setMessage] = useState<{
|
||||
type: 'success' | 'error';
|
||||
text: string;
|
||||
} | null>(null);
|
||||
const [testingConnection, setTestingConnection] = useState(false);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [deletingTasks, setDeletingTasks] = useState(false);
|
||||
|
||||
// Charger la configuration existante
|
||||
useEffect(() => {
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const result = await getTfsConfig();
|
||||
if (result.success) {
|
||||
setConfig(result.data);
|
||||
// Afficher le formulaire par défaut si TFS n'est pas configuré
|
||||
const isConfigured =
|
||||
result.data?.enabled &&
|
||||
result.data?.organizationUrl &&
|
||||
result.data?.personalAccessToken;
|
||||
if (!isConfigured) {
|
||||
setShowForm(true);
|
||||
}
|
||||
} else {
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: result.error || 'Erreur lors du chargement de la configuration',
|
||||
});
|
||||
setShowForm(true); // Afficher le formulaire en cas d'erreur
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur chargement config TFS:', error);
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: 'Erreur lors du chargement de la configuration',
|
||||
});
|
||||
setShowForm(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveConfig = () => {
|
||||
startTransition(async () => {
|
||||
setMessage(null);
|
||||
const result = await saveTfsConfig(config);
|
||||
|
||||
if (result.success) {
|
||||
setMessage({
|
||||
type: 'success',
|
||||
text: result.message || 'Configuration sauvegardée',
|
||||
});
|
||||
// Masquer le formulaire après une sauvegarde réussie
|
||||
setShowForm(false);
|
||||
} else {
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: result.error || 'Erreur lors de la sauvegarde',
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!confirm('Êtes-vous sûr de vouloir supprimer la configuration TFS ?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
setMessage(null);
|
||||
// Réinitialiser la config
|
||||
const resetConfig = {
|
||||
enabled: false,
|
||||
organizationUrl: '',
|
||||
projectName: '',
|
||||
personalAccessToken: '',
|
||||
repositories: [],
|
||||
ignoredRepositories: [],
|
||||
};
|
||||
|
||||
const result = await saveTfsConfig(resetConfig);
|
||||
|
||||
if (result.success) {
|
||||
setConfig(resetConfig);
|
||||
setMessage({ type: 'success', text: 'Configuration TFS supprimée' });
|
||||
setShowForm(true); // Afficher le formulaire pour reconfigurer
|
||||
} else {
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: result.error || 'Erreur lors de la suppression',
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const testConnection = async () => {
|
||||
try {
|
||||
setTestingConnection(true);
|
||||
setMessage(null);
|
||||
|
||||
// Sauvegarder d'abord la config
|
||||
const saveResult = await saveTfsConfig(config);
|
||||
if (!saveResult.success) {
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: saveResult.error || 'Erreur lors de la sauvegarde',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Attendre un peu que la configuration soit prise en compte
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
// Tester la connexion avec la route dédiée
|
||||
const response = await fetch('/api/tfs/test', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Test TFS - Réponse:', { status: response.status, result });
|
||||
|
||||
if (response.ok && result.connected) {
|
||||
setMessage({
|
||||
type: 'success',
|
||||
text: `Connexion Azure DevOps réussie ! ${result.message || ''}`,
|
||||
});
|
||||
} else {
|
||||
const errorMessage =
|
||||
result.error || result.details || 'Erreur de connexion inconnue';
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: `Connexion échouée: ${errorMessage}`,
|
||||
});
|
||||
console.error('Test TFS échoué:', result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur test connexion TFS:', error);
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: `Erreur réseau: ${error instanceof Error ? error.message : 'Erreur inconnue'}`,
|
||||
});
|
||||
} finally {
|
||||
setTestingConnection(false);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
setDeletingTasks(true);
|
||||
setMessage(null);
|
||||
|
||||
const result = await deleteAllTfsTasks();
|
||||
|
||||
if (result.success) {
|
||||
setMessage({
|
||||
type: 'success',
|
||||
text:
|
||||
result.message ||
|
||||
'Toutes les tâches TFS ont été supprimées avec succès',
|
||||
});
|
||||
} else {
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: result.error || 'Erreur lors de la suppression des tâches TFS',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur suppression tâches TFS:', error);
|
||||
setMessage({
|
||||
type: 'error',
|
||||
text: `Erreur réseau: ${error instanceof Error ? error.message : 'Erreur inconnue'}`,
|
||||
});
|
||||
} finally {
|
||||
setDeletingTasks(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateConfig = (
|
||||
field: keyof TfsConfig,
|
||||
value: string | boolean | string[]
|
||||
) => {
|
||||
setConfig((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const updateArrayField = (
|
||||
field: 'repositories' | 'ignoredRepositories',
|
||||
value: string
|
||||
) => {
|
||||
const array = value
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item);
|
||||
updateConfig(field, array);
|
||||
};
|
||||
|
||||
const isTfsConfigured =
|
||||
config?.enabled && config?.organizationUrl && config?.personalAccessToken;
|
||||
const isLoadingState = isLoading || isPending || deletingTasks;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="text-sm text-[var(--muted-foreground)]">
|
||||
Chargement...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Statut actuel */}
|
||||
<div className="flex items-center justify-between p-4 bg-[var(--card)] rounded border">
|
||||
<div>
|
||||
<h3 className="font-medium">Statut de l'intégration</h3>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
{isTfsConfigured
|
||||
? 'Azure DevOps est configuré et prêt à être utilisé'
|
||||
: "Azure DevOps n'est pas configuré"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant={isTfsConfigured ? 'success' : 'danger'}>
|
||||
{isTfsConfigured ? '✓ Configuré' : '✗ Non configuré'}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setShowForm(!showForm)}
|
||||
>
|
||||
{showForm ? 'Masquer' : isTfsConfigured ? 'Modifier' : 'Configurer'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isTfsConfigured && (
|
||||
<div className="p-4 bg-[var(--card)] rounded border">
|
||||
<h3 className="font-medium mb-2">Configuration actuelle</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div>
|
||||
<span className="text-[var(--muted-foreground)]">
|
||||
URL d'organisation:
|
||||
</span>{' '}
|
||||
<code className="bg-[var(--background)] px-2 py-1 rounded text-xs">
|
||||
{config?.organizationUrl || 'Non définie'}
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[var(--muted-foreground)]">Projet:</span>{' '}
|
||||
<code className="bg-[var(--background)] px-2 py-1 rounded text-xs">
|
||||
{config?.projectName || "Toute l'organisation"}
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[var(--muted-foreground)]">Token PAT:</span>{' '}
|
||||
<code className="bg-[var(--background)] px-2 py-1 rounded text-xs">
|
||||
{config?.personalAccessToken ? '••••••••' : 'Non défini'}
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[var(--muted-foreground)]">
|
||||
Repositories surveillés:
|
||||
</span>{' '}
|
||||
{config?.repositories && config.repositories.length > 0 ? (
|
||||
<div className="mt-1 space-x-1">
|
||||
{config.repositories.map((repo) => (
|
||||
<code
|
||||
key={repo}
|
||||
className="bg-[var(--background)] px-2 py-1 rounded text-xs"
|
||||
>
|
||||
{repo}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs">Tous les repositories</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[var(--muted-foreground)]">
|
||||
Repositories ignorés:
|
||||
</span>{' '}
|
||||
{config?.ignoredRepositories &&
|
||||
config.ignoredRepositories.length > 0 ? (
|
||||
<div className="mt-1 space-x-1">
|
||||
{config.ignoredRepositories.map((repo) => (
|
||||
<code
|
||||
key={repo}
|
||||
className="bg-[var(--background)] px-2 py-1 rounded text-xs"
|
||||
>
|
||||
{repo}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs">Aucun</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions de gestion des données TFS */}
|
||||
{isTfsConfigured && (
|
||||
<div className="p-4 bg-[var(--card)] rounded border border-orange-200 dark:border-orange-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-medium text-orange-800 dark:text-orange-200">
|
||||
⚠️ Gestion des données
|
||||
</h3>
|
||||
<p className="text-sm text-orange-600 dark:text-orange-300">
|
||||
Supprimez toutes les tâches TFS synchronisées de la base locale
|
||||
</p>
|
||||
<p className="text-xs text-orange-500 dark:text-orange-400 mt-1">
|
||||
<strong>Attention:</strong> Cette action est irréversible et
|
||||
supprimera définitivement toutes les tâches importées depuis
|
||||
Azure DevOps.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="danger"
|
||||
onClick={handleDeleteAllTasks}
|
||||
disabled={deletingTasks}
|
||||
className="px-6"
|
||||
>
|
||||
{deletingTasks
|
||||
? 'Suppression...'
|
||||
: '🗑️ Supprimer toutes les tâches TFS'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Formulaire de configuration */}
|
||||
{showForm && (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSaveConfig();
|
||||
}}
|
||||
className="space-y-4"
|
||||
>
|
||||
{/* Toggle d'activation */}
|
||||
<div className="flex items-center justify-between p-4 bg-[var(--muted)] rounded-lg">
|
||||
<div>
|
||||
<h4 className="font-medium">Activer l'intégration TFS</h4>
|
||||
<p className="text-sm text-[var(--muted-foreground)]">
|
||||
Synchroniser les Pull Requests depuis Azure DevOps
|
||||
</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enabled}
|
||||
onChange={(e) => updateConfig('enabled', e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{config.enabled && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
URL de l'organisation Azure DevOps
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
value={config.organizationUrl || ''}
|
||||
onChange={(e) =>
|
||||
updateConfig('organizationUrl', e.target.value)
|
||||
}
|
||||
placeholder="https://dev.azure.com/votre-organisation"
|
||||
className="w-full px-3 py-2 border border-[var(--border)] rounded bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||
L'URL de base de votre organisation Azure DevOps (ex:
|
||||
https://dev.azure.com/monentreprise)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Nom du projet (optionnel)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.projectName || ''}
|
||||
onChange={(e) => updateConfig('projectName', e.target.value)}
|
||||
placeholder="MonProjet (laisser vide pour toute l'organisation)"
|
||||
className="w-full px-3 py-2 border border-[var(--border)] rounded bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent"
|
||||
/>
|
||||
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||
Nom du projet spécifique ou laisser vide pour synchroniser les
|
||||
PRs de toute l'organisation
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Personal Access Token (PAT)
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={config.personalAccessToken || ''}
|
||||
onChange={(e) =>
|
||||
updateConfig('personalAccessToken', e.target.value)
|
||||
}
|
||||
placeholder="Votre token d'accès personnel"
|
||||
className="w-full px-3 py-2 border border-[var(--border)] rounded bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||
Créez un PAT depuis{' '}
|
||||
<a
|
||||
href="https://dev.azure.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[var(--primary)] hover:underline"
|
||||
>
|
||||
Azure DevOps
|
||||
</a>{' '}
|
||||
avec les permissions Code (read) et Pull Request (read)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Repositories à surveiller (optionnel)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.repositories?.join(', ') || ''}
|
||||
onChange={(e) =>
|
||||
updateArrayField('repositories', e.target.value)
|
||||
}
|
||||
placeholder="repo1, repo2, repo3"
|
||||
className="w-full px-3 py-2 border border-[var(--border)] rounded bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent"
|
||||
/>
|
||||
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||
Liste séparée par des virgules. Laisser vide pour surveiller
|
||||
tous les repositories.
|
||||
</p>
|
||||
{config.repositories && config.repositories.length > 0 && (
|
||||
<div className="mt-2 space-x-1">
|
||||
<span className="text-xs text-[var(--muted-foreground)]">
|
||||
Repositories surveillés:
|
||||
</span>
|
||||
{config.repositories.map((repo) => (
|
||||
<code
|
||||
key={repo}
|
||||
className="bg-[var(--muted)] text-[var(--muted-foreground)] px-2 py-1 rounded text-xs"
|
||||
>
|
||||
{repo}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Repositories à ignorer (optionnel)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.ignoredRepositories?.join(', ') || ''}
|
||||
onChange={(e) =>
|
||||
updateArrayField('ignoredRepositories', e.target.value)
|
||||
}
|
||||
placeholder="test-repo, demo-repo"
|
||||
className="w-full px-3 py-2 border border-[var(--border)] rounded bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent"
|
||||
/>
|
||||
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||
Repositories à exclure de la synchronisation, séparés par des
|
||||
virgules (ex: test-repo, demo-repo).
|
||||
</p>
|
||||
{config.ignoredRepositories &&
|
||||
config.ignoredRepositories.length > 0 && (
|
||||
<div className="mt-2 space-x-1">
|
||||
<span className="text-xs text-[var(--muted-foreground)]">
|
||||
Repositories ignorés:
|
||||
</span>
|
||||
{config.ignoredRepositories.map((repo) => (
|
||||
<code
|
||||
key={repo}
|
||||
className="bg-[var(--muted)] text-[var(--muted-foreground)] px-2 py-1 rounded text-xs"
|
||||
>
|
||||
{repo}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button type="submit" disabled={isLoadingState} className="flex-1">
|
||||
{isLoadingState
|
||||
? 'Sauvegarde...'
|
||||
: 'Sauvegarder la configuration'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={testConnection}
|
||||
disabled={
|
||||
testingConnection ||
|
||||
!config.organizationUrl ||
|
||||
!config.personalAccessToken
|
||||
}
|
||||
className="px-6"
|
||||
>
|
||||
{testingConnection ? 'Test...' : 'Tester'}
|
||||
</Button>
|
||||
|
||||
{isTfsConfigured && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handleDelete}
|
||||
disabled={isLoadingState}
|
||||
className="px-6"
|
||||
>
|
||||
Supprimer
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="p-4 bg-[var(--card)] rounded border border-dashed border-[var(--border)]">
|
||||
<h3 className="font-medium mb-2">
|
||||
💡 Instructions de configuration
|
||||
</h3>
|
||||
<div className="text-sm text-[var(--muted-foreground)] space-y-2">
|
||||
<p>
|
||||
<strong>1. URL d'organisation:</strong> Votre domaine Azure
|
||||
DevOps (ex: https://dev.azure.com/monentreprise)
|
||||
</p>
|
||||
<p>
|
||||
<strong>2. Nom du projet (optionnel):</strong> Spécifiez un
|
||||
projet pour limiter la synchronisation, ou laissez vide pour
|
||||
toute l'organisation
|
||||
</p>
|
||||
<p>
|
||||
<strong>3. Personal Access Token:</strong> Créez un PAT depuis
|
||||
Azure DevOps :
|
||||
</p>
|
||||
<ul className="ml-4 space-y-1 list-disc">
|
||||
<li>
|
||||
Allez sur{' '}
|
||||
<a
|
||||
href="https://dev.azure.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[var(--primary)] hover:underline"
|
||||
>
|
||||
dev.azure.com
|
||||
</a>
|
||||
</li>
|
||||
<li>Cliquez sur votre profil » Personal access tokens</li>
|
||||
<li>Cliquez sur "New Token"</li>
|
||||
<li>
|
||||
Sélectionnez les scopes: Code (read) et Pull Request (read)
|
||||
</li>
|
||||
<li>Copiez le token généré</li>
|
||||
</ul>
|
||||
<p className="mt-3 text-xs">
|
||||
<strong>🎯 Synchronisation intelligente:</strong> TowerControl
|
||||
récupère automatiquement toutes les Pull Requests vous
|
||||
concernant (créées par vous ou où vous êtes reviewer) dans
|
||||
l'organisation ou le projet configuré.
|
||||
</p>
|
||||
<p className="text-xs">
|
||||
<strong>Note:</strong> Les PRs seront synchronisées comme tâches
|
||||
pour un suivi centralisé de vos activités.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{message && (
|
||||
<div
|
||||
className={`p-4 rounded border ${
|
||||
message.type === 'success'
|
||||
? 'bg-green-50 border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-200'
|
||||
: 'bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-200'
|
||||
}`}
|
||||
>
|
||||
{message.text}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user