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.
This commit is contained in:
2
TODO.md
2
TODO.md
@@ -147,7 +147,7 @@
|
|||||||
## Autre Todo
|
## Autre Todo
|
||||||
- [x] Avoir un bouton pour réduire/agrandir la font des taches dans les kanban (swimlane et classique)
|
- [x] Avoir un bouton pour réduire/agrandir la font des taches dans les kanban (swimlane et classique)
|
||||||
- [ ] Refactorer les couleurs des priorités dans un seul endroit
|
- [ ] Refactorer les couleurs des priorités dans un seul endroit
|
||||||
- [ ] Settings synchro Jira : ajouter une liste de projet à ignorer, doit etre pris en compte par le service bien sur
|
- [x] Settings synchro Jira : ajouter une liste de projet à ignorer, doit etre pris en compte par le service bien sur
|
||||||
- [ ] Système de sauvegarde automatique base de données
|
- [ ] Système de sauvegarde automatique base de données
|
||||||
- [ ] Sauvegarde automatique toutes les 6 heures (configurable)
|
- [ ] Sauvegarde automatique toutes les 6 heures (configurable)
|
||||||
- [ ] Configuration dans les paramètres (intervalle de temps + bouton sauvegarde manuelle)
|
- [ ] Configuration dans les paramètres (intervalle de temps + bouton sauvegarde manuelle)
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ export function JiraConfigForm() {
|
|||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
email: '',
|
email: '',
|
||||||
apiToken: ''
|
apiToken: '',
|
||||||
|
ignoredProjects: [] as string[]
|
||||||
});
|
});
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
|
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
|
||||||
@@ -22,7 +23,8 @@ export function JiraConfigForm() {
|
|||||||
setFormData({
|
setFormData({
|
||||||
baseUrl: config.baseUrl || '',
|
baseUrl: config.baseUrl || '',
|
||||||
email: config.email || '',
|
email: config.email || '',
|
||||||
apiToken: config.apiToken || ''
|
apiToken: config.apiToken || '',
|
||||||
|
ignoredProjects: config.ignoredProjects || []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [config]);
|
}, [config]);
|
||||||
@@ -71,7 +73,8 @@ export function JiraConfigForm() {
|
|||||||
setFormData({
|
setFormData({
|
||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
email: '',
|
email: '',
|
||||||
apiToken: ''
|
apiToken: '',
|
||||||
|
ignoredProjects: []
|
||||||
});
|
});
|
||||||
setMessage({
|
setMessage({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@@ -136,6 +139,20 @@ export function JiraConfigForm() {
|
|||||||
{config?.apiToken ? '••••••••' : 'Non défini'}
|
{config?.apiToken ? '••••••••' : 'Non défini'}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -201,6 +218,39 @@ export function JiraConfigForm() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<div className="flex gap-3">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export interface JiraConfig {
|
|||||||
email?: string;
|
email?: string;
|
||||||
apiToken?: string;
|
apiToken?: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
ignoredProjects?: string[]; // Liste des clés de projets à ignorer (ex: ["DEMO", "TEST"])
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserPreferences {
|
export interface UserPreferences {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface JiraConfig {
|
|||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
email: string;
|
email: string;
|
||||||
apiToken: string;
|
apiToken: string;
|
||||||
|
ignoredProjects?: string[]; // Liste des clés de projets à ignorer (ex: ["DEMO", "TEST"])
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JiraSyncAction {
|
export interface JiraSyncAction {
|
||||||
@@ -56,6 +57,28 @@ export class JiraService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filtre les tâches Jira selon les projets ignorés
|
||||||
|
*/
|
||||||
|
private filterIgnoredProjects(jiraTasks: JiraTask[]): JiraTask[] {
|
||||||
|
if (!this.config.ignoredProjects || this.config.ignoredProjects.length === 0) {
|
||||||
|
return jiraTasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ignoredSet = new Set(this.config.ignoredProjects.map(p => p.toUpperCase()));
|
||||||
|
|
||||||
|
return jiraTasks.filter(task => {
|
||||||
|
const projectKey = task.project.key.toUpperCase();
|
||||||
|
const shouldIgnore = ignoredSet.has(projectKey);
|
||||||
|
|
||||||
|
if (shouldIgnore) {
|
||||||
|
console.log(`🚫 Ticket ${task.key} ignoré (projet ${task.project.key} dans la liste d'exclusion)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !shouldIgnore;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère les tickets assignés à l'utilisateur connecté avec pagination
|
* Récupère les tickets assignés à l'utilisateur connecté avec pagination
|
||||||
*/
|
*/
|
||||||
@@ -207,11 +230,15 @@ export class JiraService {
|
|||||||
|
|
||||||
console.log(`📋 ${jiraTasks.length} tickets trouvés dans Jira`);
|
console.log(`📋 ${jiraTasks.length} tickets trouvés dans Jira`);
|
||||||
|
|
||||||
// Récupérer la liste des IDs Jira actuels pour le nettoyage
|
// Filtrer les tâches selon les projets ignorés
|
||||||
const currentJiraIds = new Set(jiraTasks.map(task => task.id));
|
const filteredTasks = this.filterIgnoredProjects(jiraTasks);
|
||||||
|
console.log(`🔽 ${filteredTasks.length} tickets après filtrage des projets ignorés (${jiraTasks.length - filteredTasks.length} ignorés)`);
|
||||||
|
|
||||||
|
// Récupérer la liste des IDs Jira actuels pour le nettoyage (après filtrage)
|
||||||
|
const currentJiraIds = new Set(filteredTasks.map(task => task.id));
|
||||||
|
|
||||||
// Synchroniser chaque ticket
|
// Synchroniser chaque ticket
|
||||||
for (const jiraTask of jiraTasks) {
|
for (const jiraTask of filteredTasks) {
|
||||||
try {
|
try {
|
||||||
const syncAction = await this.syncSingleTask(jiraTask);
|
const syncAction = await this.syncSingleTask(jiraTask);
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ const DEFAULT_PREFERENCES: UserPreferences = {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
email: '',
|
email: '',
|
||||||
apiToken: ''
|
apiToken: '',
|
||||||
|
ignoredProjects: []
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { createJiraService } from '@/services/jira';
|
import { createJiraService, JiraService } from '@/services/jira';
|
||||||
|
import { userPreferencesService } from '@/services/user-preferences';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Route POST /api/jira/sync
|
* Route POST /api/jira/sync
|
||||||
@@ -7,11 +8,27 @@ import { createJiraService } from '@/services/jira';
|
|||||||
*/
|
*/
|
||||||
export async function POST() {
|
export async function POST() {
|
||||||
try {
|
try {
|
||||||
const jiraService = createJiraService();
|
// Essayer d'abord la config depuis la base de données
|
||||||
|
const jiraConfig = await userPreferencesService.getJiraConfig();
|
||||||
|
|
||||||
|
let jiraService: JiraService | null = null;
|
||||||
|
|
||||||
|
if (jiraConfig.enabled && jiraConfig.baseUrl && jiraConfig.email && jiraConfig.apiToken) {
|
||||||
|
// Utiliser la config depuis la base de données
|
||||||
|
jiraService = new JiraService({
|
||||||
|
baseUrl: jiraConfig.baseUrl,
|
||||||
|
email: jiraConfig.email,
|
||||||
|
apiToken: jiraConfig.apiToken,
|
||||||
|
ignoredProjects: jiraConfig.ignoredProjects || []
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback sur les variables d'environnement
|
||||||
|
jiraService = createJiraService();
|
||||||
|
}
|
||||||
|
|
||||||
if (!jiraService) {
|
if (!jiraService) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Configuration Jira manquante. Vérifiez les variables d\'environnement JIRA_BASE_URL, JIRA_EMAIL et JIRA_API_TOKEN.' },
|
{ error: 'Configuration Jira manquante. Configurez Jira dans les paramètres ou vérifiez les variables d\'environnement.' },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -64,7 +81,23 @@ export async function POST() {
|
|||||||
*/
|
*/
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const jiraService = createJiraService();
|
// Essayer d'abord la config depuis la base de données
|
||||||
|
const jiraConfig = await userPreferencesService.getJiraConfig();
|
||||||
|
|
||||||
|
let jiraService: JiraService | null = null;
|
||||||
|
|
||||||
|
if (jiraConfig.enabled && jiraConfig.baseUrl && jiraConfig.email && jiraConfig.apiToken) {
|
||||||
|
// Utiliser la config depuis la base de données
|
||||||
|
jiraService = new JiraService({
|
||||||
|
baseUrl: jiraConfig.baseUrl,
|
||||||
|
email: jiraConfig.email,
|
||||||
|
apiToken: jiraConfig.apiToken,
|
||||||
|
ignoredProjects: jiraConfig.ignoredProjects || []
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback sur les variables d'environnement
|
||||||
|
jiraService = createJiraService();
|
||||||
|
}
|
||||||
|
|
||||||
if (!jiraService) {
|
if (!jiraService) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export async function GET() {
|
|||||||
export async function PUT(request: NextRequest) {
|
export async function PUT(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { baseUrl, email, apiToken } = body;
|
const { baseUrl, email, apiToken, ignoredProjects } = body;
|
||||||
|
|
||||||
// Validation des données requises
|
// Validation des données requises
|
||||||
if (!baseUrl || !email || !apiToken) {
|
if (!baseUrl || !email || !apiToken) {
|
||||||
@@ -59,7 +59,10 @@ export async function PUT(request: NextRequest) {
|
|||||||
baseUrl: baseUrl.trim(),
|
baseUrl: baseUrl.trim(),
|
||||||
email: email.trim(),
|
email: email.trim(),
|
||||||
apiToken: apiToken.trim(),
|
apiToken: apiToken.trim(),
|
||||||
enabled: true
|
enabled: true,
|
||||||
|
ignoredProjects: Array.isArray(ignoredProjects)
|
||||||
|
? ignoredProjects.map((p: string) => p.trim().toUpperCase()).filter((p: string) => p.length > 0)
|
||||||
|
: []
|
||||||
};
|
};
|
||||||
|
|
||||||
await userPreferencesService.saveJiraConfig(jiraConfig);
|
await userPreferencesService.saveJiraConfig(jiraConfig);
|
||||||
@@ -91,7 +94,8 @@ export async function DELETE() {
|
|||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
email: '',
|
email: '',
|
||||||
apiToken: '',
|
apiToken: '',
|
||||||
enabled: false
|
enabled: false,
|
||||||
|
ignoredProjects: []
|
||||||
};
|
};
|
||||||
|
|
||||||
await userPreferencesService.saveJiraConfig(defaultConfig);
|
await userPreferencesService.saveJiraConfig(defaultConfig);
|
||||||
|
|||||||
Reference in New Issue
Block a user