feat: integrate Jira and TFS filters into KanbanFilters

- Replaced existing Jira and TFS toggle handlers with `JiraFilters` and `TfsFilters` components for improved modularity and maintainability.
- Streamlined filter management by encapsulating logic within dedicated components, enhancing readability and future extensibility.
This commit is contained in:
Julien Froidefond
2025-09-23 20:53:04 +02:00
parent db8ff88a4c
commit 336b5c1006
3 changed files with 327 additions and 272 deletions

View File

@@ -11,6 +11,8 @@ import { SORT_OPTIONS } from '@/lib/sort-config';
import { useUserPreferences } from '@/contexts/UserPreferencesContext';
import { ColumnVisibilityToggle } from './ColumnVisibilityToggle';
import { useIsMobile } from '@/hooks/useIsMobile';
import { JiraFilters } from './filters/JiraFilters';
import { TfsFilters } from './filters/TfsFilters';
export interface KanbanFilters {
search?: string;
@@ -145,126 +147,11 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
setIsSortExpanded(!isSortExpanded);
};
const handleJiraToggle = (mode: 'show' | 'hide' | 'all') => {
const updates: Partial<KanbanFilters> = {};
switch (mode) {
case 'show':
updates.showJiraOnly = true;
updates.hideJiraTasks = false;
break;
case 'hide':
updates.showJiraOnly = false;
updates.hideJiraTasks = true;
break;
case 'all':
updates.showJiraOnly = false;
updates.hideJiraTasks = false;
break;
}
onFiltersChange({ ...filters, ...updates });
};
const handleTfsToggle = (mode: 'show' | 'hide' | 'all') => {
const updates: Partial<KanbanFilters> = {};
switch (mode) {
case 'show':
updates.showTfsOnly = true;
updates.hideTfsTasks = false;
break;
case 'hide':
updates.showTfsOnly = false;
updates.hideTfsTasks = true;
break;
case 'all':
updates.showTfsOnly = false;
updates.hideTfsTasks = false;
break;
}
onFiltersChange({ ...filters, ...updates });
};
const handleJiraProjectToggle = (project: string) => {
const currentProjects = filters.jiraProjects || [];
const newProjects = currentProjects.includes(project)
? currentProjects.filter(p => p !== project)
: [...currentProjects, project];
onFiltersChange({
...filters,
jiraProjects: newProjects
});
};
const handleJiraTypeToggle = (type: string) => {
const currentTypes = filters.jiraTypes || [];
const newTypes = currentTypes.includes(type)
? currentTypes.filter(t => t !== type)
: [...currentTypes, type];
onFiltersChange({
...filters,
jiraTypes: newTypes
});
};
const handleTfsProjectToggle = (project: string) => {
const currentProjects = filters.tfsProjects || [];
const newProjects = currentProjects.includes(project)
? currentProjects.filter(p => p !== project)
: [...currentProjects, project];
onFiltersChange({
...filters,
tfsProjects: newProjects
});
};
const handleClearFilters = () => {
onFiltersChange({});
};
// Récupérer les projets et types Jira disponibles dans TOUTES les tâches (pas seulement les filtrées)
// regularTasks est déjà disponible depuis la ligne 39
const availableJiraProjects = useMemo(() => {
const projects = new Set<string>();
regularTasks.forEach(task => {
if (task.source === 'jira' && task.jiraProject) {
projects.add(task.jiraProject);
}
});
return Array.from(projects).sort();
}, [regularTasks]);
const availableJiraTypes = useMemo(() => {
const types = new Set<string>();
regularTasks.forEach(task => {
if (task.source === 'jira' && task.jiraType) {
types.add(task.jiraType);
}
});
return Array.from(types).sort();
}, [regularTasks]);
// Vérifier s'il y a des tâches Jira dans le système (même masquées)
const hasJiraTasks = regularTasks.some(task => task.source === 'jira');
// Récupérer les projets TFS disponibles dans TOUTES les tâches
const availableTfsProjects = useMemo(() => {
const projects = new Set<string>();
regularTasks.forEach(task => {
if (task.source === 'tfs' && task.tfsProject) {
projects.add(task.tfsProject);
}
});
return Array.from(projects).sort();
}, [regularTasks]);
// Vérifier s'il y a des tâches TFS dans le système (même masquées)
const hasTfsTasks = regularTasks.some(task => task.source === 'tfs');
// Calculer les compteurs pour les priorités
const priorityCounts = useMemo(() => {
@@ -464,164 +351,17 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
)}
</div>
{/* Filtres Jira - Ligne séparée mais intégrée */}
{hasJiraTasks && (
<div className="border-t border-[var(--border)]/30 pt-4 mt-4">
<div className="flex items-center gap-4 mb-3">
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
🔌 Jira
</label>
{/* Toggle Jira Show/Hide - inline avec le titre */}
<div className="flex gap-1">
<Button
variant={filters.showJiraOnly ? "primary" : "ghost"}
onClick={() => handleJiraToggle('show')}
size="sm"
className="text-xs px-2 py-1 h-auto"
>
🔹 Seul
</Button>
<Button
variant={filters.hideJiraTasks ? "danger" : "ghost"}
onClick={() => handleJiraToggle('hide')}
size="sm"
className="text-xs px-2 py-1 h-auto"
>
🚫 Mask
</Button>
<Button
variant={(!filters.showJiraOnly && !filters.hideJiraTasks) ? "primary" : "ghost"}
onClick={() => handleJiraToggle('all')}
size="sm"
className="text-xs px-2 py-1 h-auto"
>
📋 All
</Button>
</div>
</div>
{/* Filtres Jira */}
<JiraFilters
filters={filters}
onFiltersChange={onFiltersChange}
/>
{/* Projets et Types en 2 colonnes */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Projets Jira */}
{availableJiraProjects.length > 0 && (
<div>
<label className="block text-xs font-medium text-[var(--muted-foreground)] mb-2">
Projets
</label>
<div className="flex flex-wrap gap-1">
{availableJiraProjects.map((project) => (
<button
key={project}
onClick={() => handleJiraProjectToggle(project)}
className={`px-2 py-1 rounded border transition-all text-xs font-medium ${
filters.jiraProjects?.includes(project)
? 'border-blue-400 bg-blue-400/10 text-blue-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
}`}
>
📋 {project}
</button>
))}
</div>
</div>
)}
{/* Types Jira */}
{availableJiraTypes.length > 0 && (
<div>
<label className="block text-xs font-medium text-[var(--muted-foreground)] mb-2">
Types
</label>
<div className="flex flex-wrap gap-1">
{availableJiraTypes.map((type) => (
<button
key={type}
onClick={() => handleJiraTypeToggle(type)}
className={`px-2 py-1 rounded border transition-all text-xs font-medium ${
filters.jiraTypes?.includes(type)
? 'border-purple-400 bg-purple-400/10 text-purple-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
}`}
>
{type === 'Feature' && '✨ '}
{type === 'Story' && '📖 '}
{type === 'Task' && '📝 '}
{type === 'Bug' && '🐛 '}
{type === 'Support' && '🛠️ '}
{type === 'Enabler' && '🔧 '}
{type}
</button>
))}
</div>
</div>
)}
</div>
</div>
)}
{/* Filtres TFS - Ligne séparée mais intégrée */}
{hasTfsTasks && (
<div className="border-t border-[var(--border)]/30 pt-4 mt-4">
<div className="flex items-center gap-4 mb-3">
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
📦 TFS
</label>
{/* Toggle TFS Show/Hide - inline avec le titre */}
<div className="flex gap-1">
<Button
variant={filters.showTfsOnly ? "primary" : "ghost"}
onClick={() => handleTfsToggle('show')}
size="sm"
className="text-xs px-2 py-1 h-auto"
>
🔷 Seul
</Button>
<Button
variant={filters.hideTfsTasks ? "danger" : "ghost"}
onClick={() => handleTfsToggle('hide')}
size="sm"
className="text-xs px-2 py-1 h-auto"
>
🚫 Mask
</Button>
<Button
variant={(!filters.showTfsOnly && !filters.hideTfsTasks) ? "primary" : "ghost"}
onClick={() => handleTfsToggle('all')}
size="sm"
className="text-xs px-2 py-1 h-auto"
>
📋 All
</Button>
</div>
</div>
{/* Projets TFS */}
{availableTfsProjects.length > 0 && (
<div>
<label className="block text-xs font-medium text-[var(--muted-foreground)] mb-2">
Projets
</label>
<div className="flex flex-wrap gap-1">
{availableTfsProjects.map((project) => (
<button
key={project}
onClick={() => handleTfsProjectToggle(project)}
className={`px-2 py-1 rounded border transition-all text-xs font-medium ${
filters.tfsProjects?.includes(project)
? 'border-orange-400 bg-orange-400/10 text-orange-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
}`}
>
📦 {project}
</button>
))}
</div>
</div>
)}
</div>
)}
{/* Filtres TFS */}
<TfsFilters
filters={filters}
onFiltersChange={onFiltersChange}
/>
{/* Visibilité des colonnes */}
<div className="col-span-full border-t border-[var(--border)]/50 pt-6 mt-4">

View File

@@ -0,0 +1,185 @@
'use client';
import { useMemo } from 'react';
import { Button } from '@/components/ui/Button';
import { useTasksContext } from '@/contexts/TasksContext';
import { KanbanFilters } from '../KanbanFilters';
interface JiraFiltersProps {
filters: KanbanFilters;
onFiltersChange: (filters: KanbanFilters) => void;
}
export function JiraFilters({ filters, onFiltersChange }: JiraFiltersProps) {
const { regularTasks } = useTasksContext();
// Vérifier s'il y a des tâches Jira dans le système (même masquées)
const hasJiraTasks = regularTasks.some(task => task.source === 'jira');
// Récupérer les projets et types Jira disponibles dans TOUTES les tâches
const availableJiraProjects = useMemo(() => {
const projects = new Set<string>();
regularTasks.forEach(task => {
if (task.source === 'jira' && task.jiraProject) {
projects.add(task.jiraProject);
}
});
return Array.from(projects).sort();
}, [regularTasks]);
const availableJiraTypes = useMemo(() => {
const types = new Set<string>();
regularTasks.forEach(task => {
if (task.source === 'jira' && task.jiraType) {
types.add(task.jiraType);
}
});
return Array.from(types).sort();
}, [regularTasks]);
const handleJiraToggle = (mode: 'show' | 'hide' | 'all') => {
const updates: Partial<KanbanFilters> = {};
switch (mode) {
case 'show':
updates.showJiraOnly = true;
updates.hideJiraTasks = false;
break;
case 'hide':
updates.showJiraOnly = false;
updates.hideJiraTasks = true;
break;
case 'all':
updates.showJiraOnly = false;
updates.hideJiraTasks = false;
break;
}
onFiltersChange({ ...filters, ...updates });
};
const handleJiraProjectToggle = (project: string) => {
const currentProjects = filters.jiraProjects || [];
const newProjects = currentProjects.includes(project)
? currentProjects.filter(p => p !== project)
: [...currentProjects, project];
onFiltersChange({
...filters,
jiraProjects: newProjects
});
};
const handleJiraTypeToggle = (type: string) => {
const currentTypes = filters.jiraTypes || [];
const newTypes = currentTypes.includes(type)
? currentTypes.filter(t => t !== type)
: [...currentTypes, type];
onFiltersChange({
...filters,
jiraTypes: newTypes
});
};
// Si pas de tâches Jira, on n'affiche rien
if (!hasJiraTasks) {
return null;
}
return (
<div className="border-t border-[var(--border)]/30 pt-4 mt-4">
<div className="flex items-center gap-4 mb-3">
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
🔌 Jira
</label>
{/* Toggle Jira Show/Hide - inline avec le titre */}
<div className="flex gap-1">
<Button
variant={filters.showJiraOnly ? "primary" : "ghost"}
onClick={() => handleJiraToggle('show')}
size="sm"
className="text-xs px-2 py-1 h-auto"
>
🔹 Seul
</Button>
<Button
variant={filters.hideJiraTasks ? "danger" : "ghost"}
onClick={() => handleJiraToggle('hide')}
size="sm"
className="text-xs px-2 py-1 h-auto"
>
🚫 Mask
</Button>
<Button
variant={(!filters.showJiraOnly && !filters.hideJiraTasks) ? "primary" : "ghost"}
onClick={() => handleJiraToggle('all')}
size="sm"
className="text-xs px-2 py-1 h-auto"
>
📋 All
</Button>
</div>
</div>
{/* Projets et Types en 2 colonnes */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Projets Jira */}
{availableJiraProjects.length > 0 && (
<div>
<label className="block text-xs font-medium text-[var(--muted-foreground)] mb-2">
Projets
</label>
<div className="flex flex-wrap gap-1">
{availableJiraProjects.map((project) => (
<button
key={project}
onClick={() => handleJiraProjectToggle(project)}
className={`px-2 py-1 rounded border transition-all text-xs font-medium ${
filters.jiraProjects?.includes(project)
? 'border-blue-400 bg-blue-400/10 text-blue-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
}`}
>
📋 {project}
</button>
))}
</div>
</div>
)}
{/* Types Jira */}
{availableJiraTypes.length > 0 && (
<div>
<label className="block text-xs font-medium text-[var(--muted-foreground)] mb-2">
Types
</label>
<div className="flex flex-wrap gap-1">
{availableJiraTypes.map((type) => (
<button
key={type}
onClick={() => handleJiraTypeToggle(type)}
className={`px-2 py-1 rounded border transition-all text-xs font-medium ${
filters.jiraTypes?.includes(type)
? 'border-purple-400 bg-purple-400/10 text-purple-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
}`}
>
{type === 'Feature' && '✨ '}
{type === 'Story' && '📖 '}
{type === 'Task' && '📝 '}
{type === 'Bug' && '🐛 '}
{type === 'Support' && '🛠️ '}
{type === 'Enabler' && '🔧 '}
{type}
</button>
))}
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,130 @@
'use client';
import { useMemo } from 'react';
import { Button } from '@/components/ui/Button';
import { useTasksContext } from '@/contexts/TasksContext';
import { KanbanFilters } from '../KanbanFilters';
interface TfsFiltersProps {
filters: KanbanFilters;
onFiltersChange: (filters: KanbanFilters) => void;
}
export function TfsFilters({ filters, onFiltersChange }: TfsFiltersProps) {
const { regularTasks } = useTasksContext();
// Vérifier s'il y a des tâches TFS dans le système (même masquées)
const hasTfsTasks = regularTasks.some(task => task.source === 'tfs');
// Récupérer les projets TFS disponibles dans TOUTES les tâches
const availableTfsProjects = useMemo(() => {
const projects = new Set<string>();
regularTasks.forEach(task => {
if (task.source === 'tfs' && task.tfsProject) {
projects.add(task.tfsProject);
}
});
return Array.from(projects).sort();
}, [regularTasks]);
const handleTfsToggle = (mode: 'show' | 'hide' | 'all') => {
const updates: Partial<KanbanFilters> = {};
switch (mode) {
case 'show':
updates.showTfsOnly = true;
updates.hideTfsTasks = false;
break;
case 'hide':
updates.showTfsOnly = false;
updates.hideTfsTasks = true;
break;
case 'all':
updates.showTfsOnly = false;
updates.hideTfsTasks = false;
break;
}
onFiltersChange({ ...filters, ...updates });
};
const handleTfsProjectToggle = (project: string) => {
const currentProjects = filters.tfsProjects || [];
const newProjects = currentProjects.includes(project)
? currentProjects.filter(p => p !== project)
: [...currentProjects, project];
onFiltersChange({
...filters,
tfsProjects: newProjects
});
};
// Si pas de tâches TFS, on n'affiche rien
if (!hasTfsTasks) {
return null;
}
return (
<div className="border-t border-[var(--border)]/30 pt-4 mt-4">
<div className="flex items-center gap-4 mb-3">
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
📦 TFS
</label>
{/* Toggle TFS Show/Hide - inline avec le titre */}
<div className="flex gap-1">
<Button
variant={filters.showTfsOnly ? "primary" : "ghost"}
onClick={() => handleTfsToggle('show')}
size="sm"
className="text-xs px-2 py-1 h-auto"
>
🔷 Seul
</Button>
<Button
variant={filters.hideTfsTasks ? "danger" : "ghost"}
onClick={() => handleTfsToggle('hide')}
size="sm"
className="text-xs px-2 py-1 h-auto"
>
🚫 Mask
</Button>
<Button
variant={(!filters.showTfsOnly && !filters.hideTfsTasks) ? "primary" : "ghost"}
onClick={() => handleTfsToggle('all')}
size="sm"
className="text-xs px-2 py-1 h-auto"
>
📋 All
</Button>
</div>
</div>
{/* Projets TFS */}
{availableTfsProjects.length > 0 && (
<div>
<label className="block text-xs font-medium text-[var(--muted-foreground)] mb-2">
Projets
</label>
<div className="flex flex-wrap gap-1">
{availableTfsProjects.map((project) => (
<button
key={project}
onClick={() => handleTfsProjectToggle(project)}
className={`px-2 py-1 rounded border transition-all text-xs font-medium ${
filters.tfsProjects?.includes(project)
? 'border-orange-400 bg-orange-400/10 text-orange-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
}`}
>
📦 {project}
</button>
))}
</div>
</div>
)}
</div>
);
}