Files
towercontrol/components/settings/JiraConfigForm.tsx
Julien Froidefond a98bde86d3 feat: implement project ignore list for Jira synchronization
- Updated `JiraConfigForm` to include an input for ignored projects, allowing users to specify projects to exclude from synchronization.
- Enhanced `JiraService` with a method to filter out tasks from ignored projects, improving task management.
- Modified user preferences to store ignored projects, ensuring persistence across sessions.
- Updated API routes to handle ignored projects in configuration, enhancing overall functionality.
- Marked the corresponding task as complete in TODO.md.
2025-09-18 13:29:15 +02:00

308 lines
11 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { useJiraConfig } from '@/hooks/useJiraConfig';
export function JiraConfigForm() {
const { config, isLoading: configLoading, saveConfig, deleteConfig } = useJiraConfig();
const [formData, setFormData] = useState({
baseUrl: '',
email: '',
apiToken: '',
ignoredProjects: [] as string[]
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
// Charger les données existantes dans le formulaire
useEffect(() => {
if (config) {
setFormData({
baseUrl: config.baseUrl || '',
email: config.email || '',
apiToken: config.apiToken || '',
ignoredProjects: config.ignoredProjects || []
});
}
}, [config]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
setMessage(null);
try {
const result = await saveConfig(formData);
if (result.success) {
setMessage({
type: 'success',
text: result.message
});
} else {
setMessage({
type: 'error',
text: result.message
});
}
} catch (error) {
setMessage({
type: 'error',
text: error instanceof Error ? error.message : 'Erreur lors de la sauvegarde de la configuration'
});
} finally {
setIsSubmitting(false);
}
};
const handleDelete = async () => {
if (!confirm('Êtes-vous sûr de vouloir supprimer la configuration Jira ?')) {
return;
}
setIsSubmitting(true);
setMessage(null);
try {
const result = await deleteConfig();
if (result.success) {
setFormData({
baseUrl: '',
email: '',
apiToken: '',
ignoredProjects: []
});
setMessage({
type: 'success',
text: result.message
});
} else {
setMessage({
type: 'error',
text: result.message
});
}
} catch (error) {
setMessage({
type: 'error',
text: error instanceof Error ? error.message : 'Erreur lors de la suppression de la configuration'
});
} finally {
setIsSubmitting(false);
}
};
const isJiraConfigured = config?.enabled && (config?.baseUrl || config?.email);
const isLoading = configLoading || isSubmitting;
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&apos;intégration</h3>
<p className="text-sm text-[var(--muted-foreground)]">
{isJiraConfigured
? 'Jira est configuré et prêt à être utilisé'
: 'Jira n\'est pas configuré'
}
</p>
</div>
<Badge variant={isJiraConfigured ? 'success' : 'danger'}>
{isJiraConfigured ? '✓ Configuré' : '✗ Non configuré'}
</Badge>
</div>
{isJiraConfigured && (
<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 de base:</span>{' '}
<code className="bg-[var(--background)] px-2 py-1 rounded text-xs">
{config?.baseUrl || 'Non définie'}
</code>
</div>
<div>
<span className="text-[var(--muted-foreground)]">Email:</span>{' '}
<code className="bg-[var(--background)] px-2 py-1 rounded text-xs">
{config?.email || 'Non défini'}
</code>
</div>
<div>
<span className="text-[var(--muted-foreground)]">Token API:</span>{' '}
<code className="bg-[var(--background)] px-2 py-1 rounded text-xs">
{config?.apiToken ? '••••••••' : 'Non défini'}
</code>
</div>
<div>
<span className="text-[var(--muted-foreground)]">Projets ignorés:</span>{' '}
{config?.ignoredProjects && config.ignoredProjects.length > 0 ? (
<div className="mt-1 space-x-1">
{config.ignoredProjects.map(project => (
<code key={project} className="bg-[var(--background)] px-2 py-1 rounded text-xs">
{project}
</code>
))}
</div>
) : (
<span className="text-xs">Aucun</span>
)}
</div>
</div>
</div>
)}
{/* Formulaire de configuration */}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">
URL de base Jira Cloud
</label>
<input
type="url"
value={formData.baseUrl}
onChange={(e) => setFormData(prev => ({ ...prev, baseUrl: e.target.value }))}
placeholder="https://votre-domaine.atlassian.net"
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&apos;URL de votre instance Jira Cloud (ex: https://monentreprise.atlassian.net)
</p>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Email Jira
</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
placeholder="votre-email@exemple.com"
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&apos;email de votre compte Jira
</p>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Token API Jira
</label>
<input
type="password"
value={formData.apiToken}
onChange={(e) => setFormData(prev => ({ ...prev, apiToken: e.target.value }))}
placeholder="Votre token API Jira"
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 token API depuis{' '}
<a
href="https://id.atlassian.com/manage-profile/security/api-tokens"
target="_blank"
rel="noopener noreferrer"
className="text-[var(--primary)] hover:underline"
>
votre profil Atlassian
</a>
</p>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Projets à ignorer (optionnel)
</label>
<input
type="text"
value={formData.ignoredProjects.join(', ')}
onChange={(e) => {
const projects = e.target.value
.split(',')
.map(p => p.trim().toUpperCase())
.filter(p => p.length > 0);
setFormData(prev => ({ ...prev, ignoredProjects: projects }));
}}
placeholder="DEMO, TEST, SANDBOX"
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 des clés de projets à ignorer lors de la synchronisation, séparées par des virgules (ex: DEMO, TEST, SANDBOX).
Ces projets ne seront pas synchronisés vers TowerControl.
</p>
{formData.ignoredProjects.length > 0 && (
<div className="mt-2 space-x-1">
<span className="text-xs text-[var(--muted-foreground)]">Projets qui seront ignorés:</span>
{formData.ignoredProjects.map(project => (
<code key={project} className="bg-[var(--muted)] text-[var(--muted-foreground)] px-2 py-1 rounded text-xs">
{project}
</code>
))}
</div>
)}
</div>
<div className="flex gap-3">
<Button
type="submit"
disabled={isLoading}
className="flex-1"
>
{isLoading ? 'Sauvegarde...' : 'Sauvegarder la configuration'}
</Button>
{isJiraConfigured && (
<Button
type="button"
variant="secondary"
onClick={handleDelete}
disabled={isLoading}
className="px-6"
>
Supprimer
</Button>
)}
</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>
)}
{/* 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 de base:</strong> Votre domaine Jira Cloud (ex: https://monentreprise.atlassian.net)</p>
<p><strong>2. Email:</strong> L&apos;email de votre compte Jira/Atlassian</p>
<p><strong>3. Token API:</strong> Créez un token depuis votre profil Atlassian :</p>
<ul className="ml-4 space-y-1 list-disc">
<li>Allez sur <a href="https://id.atlassian.com/manage-profile/security/api-tokens" target="_blank" rel="noopener noreferrer" className="text-[var(--primary)] hover:underline">id.atlassian.com</a></li>
<li>Cliquez sur &quot;Create API token&quot;</li>
<li>Donnez un nom descriptif (ex: &quot;TowerControl&quot;)</li>
<li>Copiez le token généré</li>
</ul>
<p className="mt-3 text-xs">
<strong>Note:</strong> Ces variables doivent être configurées dans l&apos;environnement du serveur (JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN)
</p>
</div>
</div>
</div>
);
}