feat: refactor KanbanFilters to use modular filter components

- Replaced inline priority and tag filtering logic with dedicated `PriorityFilters`, `TagFilters`, `GeneralFilters`, and `ColumnFilters` components for better organization and maintainability.
- Optimized layout to enhance responsiveness and user experience by restructuring the filter display into a grid format.
- Removed unused code related to previous filtering logic, streamlining the component.
This commit is contained in:
Julien Froidefond
2025-09-25 22:33:11 +02:00
parent f2b18e4527
commit b0e7a60308
5 changed files with 236 additions and 136 deletions

View File

@@ -6,13 +6,15 @@ import { TaskPriority, TaskStatus } from '@/lib/types';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { useTasksContext } from '@/contexts/TasksContext';
import { getAllPriorities, getPriorityColorHex } from '@/lib/status-config';
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';
import { PriorityFilters } from './filters/PriorityFilters';
import { TagFilters } from './filters/TagFilters';
import { GeneralFilters } from './filters/GeneralFilters';
import { ColumnFilters } from './filters/ColumnFilters';
export interface KanbanFilters {
search?: string;
@@ -44,7 +46,7 @@ interface KanbanFiltersProps {
}
export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsHiddenStatuses, onToggleStatusVisibility }: KanbanFiltersProps) {
const { tags: availableTags, regularTasks, activeFiltersCount } = useTasksContext();
const { regularTasks, activeFiltersCount } = useTasksContext();
const { preferences, toggleColumnVisibility } = useUserPreferences();
// Utiliser les props si disponibles, sinon utiliser le context
@@ -197,41 +199,6 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
});
};
// Calculer les compteurs pour les priorités
const priorityCounts = useMemo(() => {
const counts: Record<string, number> = {};
getAllPriorities().forEach(priority => {
counts[priority.key] = regularTasks.filter(task => task.priority === priority.key).length;
});
return counts;
}, [regularTasks]);
// Calculer les compteurs pour les tags
const tagCounts = useMemo(() => {
const counts: Record<string, number> = {};
availableTags.forEach(tag => {
counts[tag.name] = regularTasks.filter(task => task.tags?.includes(tag.name)).length;
});
return counts;
}, [regularTasks, availableTags]);
const priorityOptions = getAllPriorities().map(priorityConfig => ({
value: priorityConfig.key,
label: priorityConfig.label,
color: priorityConfig.color,
count: priorityCounts[priorityConfig.key] || 0
}));
// Trier les tags par nombre d'utilisation (décroissant)
const sortedTags = useMemo(() => {
return [...availableTags].sort((a, b) => {
const countA = tagCounts[a.name] || 0;
const countB = tagCounts[b.name] || 0;
return countB - countA; // Décroissant
});
}, [availableTags, tagCounts]);
return (
<div className="bg-[var(--card)]/50 border-b border-[var(--border)]/50 backdrop-blur-sm">
<div className="container mx-auto px-6 py-4">
@@ -339,110 +306,47 @@ export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsH
{/* Filtres étendus */}
<div className="mt-4 border-t border-[var(--border)]/50 pt-4">
{/* Grille responsive pour les filtres principaux */}
<div className="grid grid-cols-1 lg:grid-cols-[auto_1fr] gap-6 lg:gap-8">
{/* Filtres par priorité */}
<div className="space-y-3">
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
Priorités
</label>
<div className="flex flex-wrap gap-1">
{priorityOptions.filter(priority => priority.count > 0).map((priority) => (
<button
key={priority.value}
onClick={() => handlePriorityToggle(priority.value)}
className={`flex items-center gap-2 px-2 py-1 rounded border transition-all text-xs font-medium ${
filters.priorities?.includes(priority.value)
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
}`}
>
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: getPriorityColorHex(priority.color) }}
/>
{priority.label} ({priority.count})
</button>
))}
</div>
{/* Layout optimisé : 3 colonnes avec Tags très large à droite */}
<div className="grid grid-cols-1 xl:grid-cols-[auto_minmax(0,500px)_3fr] gap-4 lg:gap-6 items-start">
{/* Colonne 1 : Priorités + Généraux */}
<div className="space-y-4">
<PriorityFilters
selectedPriorities={filters.priorities}
onPriorityToggle={handlePriorityToggle}
/>
<GeneralFilters
showWithDueDate={filters.showWithDueDate}
onDueDateFilterToggle={handleDueDateFilterToggle}
/>
</div>
{/* Filtres par tags */}
{availableTags.length > 0 && (
<div className="space-y-3">
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
Tags
</label>
<div className="flex flex-wrap gap-1 max-h-32 overflow-y-auto">
{sortedTags.filter(tag => (tagCounts[tag.name] || 0) > 0).map((tag) => (
<button
key={tag.id}
onClick={() => handleTagToggle(tag.name)}
className={`flex items-center gap-2 px-2 py-1 rounded border transition-all text-xs font-medium ${
filters.tags?.includes(tag.name)
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
}`}
>
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: tag.color }}
/>
{tag.name} ({tagCounts[tag.name]})
</button>
))}
</div>
</div>
)}
</div>
{/* Colonne 2 : Tags - Espace restant maximum */}
<TagFilters
selectedTags={filters.tags}
onTagToggle={handleTagToggle}
/>
{/* Filtres généraux */}
<div className="border-t border-[var(--border)]/30 pt-4 mt-4">
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider mb-3">
Filtres généraux
</label>
<div className="flex flex-wrap gap-1">
<button
type="button"
onClick={handleDueDateFilterToggle}
className={`flex items-center gap-2 px-2 py-1 rounded border transition-all text-xs font-medium cursor-pointer ${
filters.showWithDueDate
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)] hover:bg-[var(--card)]/80'
}`}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Avec date de fin
</button>
</div>
</div>
{/* Filtres Jira */}
<JiraFilters
filters={filters}
onFiltersChange={onFiltersChange}
/>
{/* 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">
<ColumnVisibilityToggle
{/* Colonne 3 : Visibilité des colonnes */}
<ColumnFilters
hiddenStatuses={hiddenStatuses}
onToggleStatus={toggleStatusVisibility}
tasks={regularTasks}
className="text-xs"
/>
</div>
{/* Deuxième ligne : TFS et Jira côte à côte */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-2">
{/* Filtres TFS */}
<TfsFilters
filters={filters}
onFiltersChange={onFiltersChange}
/>
{/* Filtres Jira */}
<JiraFilters
filters={filters}
onFiltersChange={onFiltersChange}
/>
</div>

View File

@@ -0,0 +1,39 @@
'use client';
import { TaskStatus, Task } from '@/lib/types';
import { getAllStatuses } from '@/lib/status-config';
interface ColumnFiltersProps {
hiddenStatuses: Set<TaskStatus>;
onToggleStatus: (status: TaskStatus) => void;
tasks: Task[];
}
export function ColumnFilters({ hiddenStatuses, onToggleStatus, tasks }: ColumnFiltersProps) {
return (
<div className="space-y-2">
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
Colonnes
</label>
<div className="flex flex-wrap gap-1">
{getAllStatuses().map(statusConfig => {
const statusCount = tasks.filter(task => task.status === statusConfig.key).length;
return (
<button
key={statusConfig.key}
onClick={() => onToggleStatus(statusConfig.key)}
className={`px-2 py-1 rounded border text-xs font-medium transition-colors ${
hiddenStatuses.has(statusConfig.key)
? 'bg-[var(--muted)]/20 text-[var(--muted)] border-[var(--muted)]/30 hover:bg-[var(--muted)]/30'
: 'bg-[var(--primary)]/20 text-[var(--primary)] border-[var(--primary)]/30 hover:bg-[var(--primary)]/30'
}`}
title={hiddenStatuses.has(statusConfig.key) ? `Afficher ${statusConfig.label}` : `Masquer ${statusConfig.label}`}
>
{hiddenStatuses.has(statusConfig.key) ? '👁️‍🗨️' : '👁️'} {statusConfig.label}{statusCount ? ` (${statusCount})` : ''}
</button>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,37 @@
'use client';
interface GeneralFiltersProps {
showWithDueDate?: boolean;
onDueDateFilterToggle: () => void;
}
export function GeneralFilters({ showWithDueDate = false, onDueDateFilterToggle }: GeneralFiltersProps) {
return (
<div className="space-y-2">
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
Généraux
</label>
<div className="flex flex-wrap gap-1">
<button
type="button"
onClick={onDueDateFilterToggle}
className={`flex items-center gap-2 px-2 py-1 rounded border transition-all text-xs font-medium cursor-pointer ${
showWithDueDate
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)] hover:bg-[var(--card)]/80'
}`}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 002 2v12a2 2 0 002 2z" />
</svg>
Avec date de fin
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,58 @@
'use client';
import { useMemo } from 'react';
import { TaskPriority } from '@/lib/types';
import { useTasksContext } from '@/contexts/TasksContext';
import { getAllPriorities, getPriorityColorHex } from '@/lib/status-config';
interface PriorityFiltersProps {
selectedPriorities?: TaskPriority[];
onPriorityToggle: (priority: TaskPriority) => void;
}
export function PriorityFilters({ selectedPriorities = [], onPriorityToggle }: PriorityFiltersProps) {
const { regularTasks } = useTasksContext();
// Calculer les compteurs pour les priorités
const priorityCounts = useMemo(() => {
const counts: Record<string, number> = {};
getAllPriorities().forEach(priority => {
counts[priority.key] = regularTasks.filter(task => task.priority === priority.key).length;
});
return counts;
}, [regularTasks]);
const priorityOptions = getAllPriorities().map(priorityConfig => ({
value: priorityConfig.key,
label: priorityConfig.label,
color: priorityConfig.color,
count: priorityCounts[priorityConfig.key] || 0
}));
return (
<div className="space-y-2">
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
Priorités
</label>
<div className="flex flex-wrap gap-1">
{priorityOptions.filter(priority => priority.count > 0).map((priority) => (
<button
key={priority.value}
onClick={() => onPriorityToggle(priority.value)}
className={`flex items-center gap-2 px-2 py-1 rounded border transition-all text-xs font-medium ${
selectedPriorities.includes(priority.value)
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
}`}
>
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: getPriorityColorHex(priority.color) }}
/>
{priority.label} ({priority.count})
</button>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,62 @@
'use client';
import { useMemo } from 'react';
import { useTasksContext } from '@/contexts/TasksContext';
interface TagFiltersProps {
selectedTags?: string[];
onTagToggle: (tagName: string) => void;
}
export function TagFilters({ selectedTags = [], onTagToggle }: TagFiltersProps) {
const { tags: availableTags, regularTasks } = useTasksContext();
// Calculer les compteurs pour les tags
const tagCounts = useMemo(() => {
const counts: Record<string, number> = {};
availableTags.forEach(tag => {
counts[tag.name] = regularTasks.filter(task => task.tags?.includes(tag.name)).length;
});
return counts;
}, [regularTasks, availableTags]);
// Trier les tags par nombre d'utilisation (décroissant)
const sortedTags = useMemo(() => {
return [...availableTags].sort((a, b) => {
const countA = tagCounts[a.name] || 0;
const countB = tagCounts[b.name] || 0;
return countB - countA; // Décroissant
});
}, [availableTags, tagCounts]);
if (availableTags.length === 0) {
return null;
}
return (
<div className="space-y-2">
<label className="block text-xs font-mono font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
Tags
</label>
<div className="flex flex-wrap gap-1 max-h-32 overflow-y-auto">
{sortedTags.filter(tag => (tagCounts[tag.name] || 0) > 0).map((tag) => (
<button
key={tag.id}
onClick={() => onTagToggle(tag.name)}
className={`flex items-center gap-2 px-2 py-1 rounded border transition-all text-xs font-medium ${
selectedTags.includes(tag.name)
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
: 'border-[var(--border)] bg-[var(--card)] text-[var(--muted-foreground)] hover:border-[var(--border)]'
}`}
>
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: tag.color }}
/>
{tag.name} ({tagCounts[tag.name]})
</button>
))}
</div>
</div>
);
}