- Modified KanbanFilters to set tags and priorities directly, removing the conditional checks for empty arrays. This ensures that the filters are always updated with the latest values, improving consistency in filter application.
666 lines
27 KiB
TypeScript
666 lines
27 KiB
TypeScript
'use client';
|
||
|
||
import { useState, useEffect, useRef, useMemo } from 'react';
|
||
import { createPortal } from 'react-dom';
|
||
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';
|
||
|
||
export interface KanbanFilters {
|
||
search?: string;
|
||
tags?: string[];
|
||
priorities?: TaskPriority[];
|
||
showCompleted?: boolean;
|
||
compactView?: boolean;
|
||
swimlanesByTags?: boolean;
|
||
swimlanesMode?: 'tags' | 'priority'; // Mode des swimlanes
|
||
pinnedTag?: string; // Tag pour les objectifs principaux
|
||
sortBy?: string; // Clé de l'option de tri sélectionnée
|
||
// Filtres spécifiques Jira
|
||
showJiraOnly?: boolean; // Afficher seulement les tâches Jira
|
||
hideJiraTasks?: boolean; // Masquer toutes les tâches Jira
|
||
jiraProjects?: string[]; // Filtrer par projet Jira
|
||
jiraTypes?: string[]; // Filtrer par type Jira (Story, Task, Bug, etc.)
|
||
}
|
||
|
||
interface KanbanFiltersProps {
|
||
filters: KanbanFilters;
|
||
onFiltersChange: (filters: KanbanFilters) => void;
|
||
hiddenStatuses?: Set<TaskStatus>;
|
||
onToggleStatusVisibility?: (status: TaskStatus) => void;
|
||
}
|
||
|
||
export function KanbanFilters({ filters, onFiltersChange, hiddenStatuses: propsHiddenStatuses, onToggleStatusVisibility }: KanbanFiltersProps) {
|
||
const { tags: availableTags, regularTasks, activeFiltersCount } = useTasksContext();
|
||
const { preferences, toggleColumnVisibility } = useUserPreferences();
|
||
|
||
// Utiliser les props si disponibles, sinon utiliser le context
|
||
const hiddenStatuses = propsHiddenStatuses || new Set(preferences.columnVisibility.hiddenStatuses);
|
||
const toggleStatusVisibility = onToggleStatusVisibility || toggleColumnVisibility;
|
||
const [isExpanded, setIsExpanded] = useState(false);
|
||
const [isSortExpanded, setIsSortExpanded] = useState(false);
|
||
const [isSwimlaneModeExpanded, setIsSwimlaneModeExpanded] = useState(false);
|
||
const sortDropdownRef = useRef<HTMLDivElement>(null);
|
||
const swimlaneModeDropdownRef = useRef<HTMLDivElement>(null);
|
||
const sortButtonRef = useRef<HTMLButtonElement>(null);
|
||
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
|
||
|
||
// Fermer les dropdowns en cliquant à l'extérieur
|
||
useEffect(() => {
|
||
function handleClickOutside(event: MouseEvent) {
|
||
if (sortDropdownRef.current && !sortDropdownRef.current.contains(event.target as Node)) {
|
||
setIsSortExpanded(false);
|
||
}
|
||
if (swimlaneModeDropdownRef.current && !swimlaneModeDropdownRef.current.contains(event.target as Node)) {
|
||
setIsSwimlaneModeExpanded(false);
|
||
}
|
||
}
|
||
|
||
if (isSortExpanded || isSwimlaneModeExpanded) {
|
||
document.addEventListener('mousedown', handleClickOutside);
|
||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||
}
|
||
}, [isSortExpanded, isSwimlaneModeExpanded]);
|
||
|
||
const handleSearchChange = (search: string) => {
|
||
onFiltersChange({ ...filters, search: search || undefined });
|
||
};
|
||
|
||
const handleTagToggle = (tagName: string) => {
|
||
const currentTags = filters.tags || [];
|
||
const newTags = currentTags.includes(tagName)
|
||
? currentTags.filter(t => t !== tagName)
|
||
: [...currentTags, tagName];
|
||
|
||
onFiltersChange({
|
||
...filters,
|
||
tags: newTags
|
||
});
|
||
};
|
||
|
||
const handlePriorityToggle = (priority: TaskPriority) => {
|
||
const currentPriorities = filters.priorities || [];
|
||
const newPriorities = currentPriorities.includes(priority)
|
||
? currentPriorities.filter(p => p !== priority)
|
||
: [...currentPriorities, priority];
|
||
|
||
onFiltersChange({
|
||
...filters,
|
||
priorities: newPriorities
|
||
});
|
||
};
|
||
|
||
|
||
const handleSwimlanesToggle = () => {
|
||
onFiltersChange({
|
||
...filters,
|
||
swimlanesByTags: !filters.swimlanesByTags
|
||
});
|
||
};
|
||
|
||
const handleSwimlaneModeChange = (mode: 'tags' | 'priority') => {
|
||
onFiltersChange({
|
||
...filters,
|
||
swimlanesByTags: true,
|
||
swimlanesMode: mode
|
||
});
|
||
setIsSwimlaneModeExpanded(false);
|
||
};
|
||
|
||
const handleSwimlaneModeToggle = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||
const button = event.currentTarget;
|
||
const rect = button.getBoundingClientRect();
|
||
setDropdownPosition({
|
||
top: rect.bottom + window.scrollY + 4,
|
||
left: rect.left + window.scrollX
|
||
});
|
||
setIsSwimlaneModeExpanded(!isSwimlaneModeExpanded);
|
||
};
|
||
|
||
const handleSortChange = (sortKey: string) => {
|
||
onFiltersChange({
|
||
...filters,
|
||
sortBy: sortKey
|
||
});
|
||
};
|
||
|
||
const handleSortToggle = () => {
|
||
if (!isSortExpanded && sortButtonRef.current) {
|
||
const rect = sortButtonRef.current.getBoundingClientRect();
|
||
setDropdownPosition({
|
||
top: rect.bottom + window.scrollY + 4,
|
||
left: rect.left + window.scrollX
|
||
});
|
||
}
|
||
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 handleJiraProjectToggle = (project: string) => {
|
||
const currentProjects = filters.jiraProjects || [];
|
||
const newProjects = currentProjects.includes(project)
|
||
? currentProjects.filter(p => p !== project)
|
||
: [...currentProjects, project];
|
||
|
||
onFiltersChange({
|
||
...filters,
|
||
jiraProjects: newProjects.length > 0 ? newProjects : undefined
|
||
});
|
||
};
|
||
|
||
const handleJiraTypeToggle = (type: string) => {
|
||
const currentTypes = filters.jiraTypes || [];
|
||
const newTypes = currentTypes.includes(type)
|
||
? currentTypes.filter(t => t !== type)
|
||
: [...currentTypes, type];
|
||
|
||
onFiltersChange({
|
||
...filters,
|
||
jiraTypes: newTypes.length > 0 ? newTypes : undefined
|
||
});
|
||
};
|
||
|
||
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');
|
||
|
||
// 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">
|
||
{/* Header avec recherche et bouton expand */}
|
||
<div className="flex items-center gap-4">
|
||
<div className="flex-1 max-w-md">
|
||
<Input
|
||
type="text"
|
||
value={filters.search || ''}
|
||
onChange={(e) => handleSearchChange(e.target.value)}
|
||
placeholder="Rechercher des tâches..."
|
||
className="bg-[var(--card)] border-[var(--border)]"
|
||
/>
|
||
</div>
|
||
|
||
{/* Menu swimlanes */}
|
||
<div className="flex gap-1">
|
||
<Button
|
||
variant={filters.swimlanesByTags ? "primary" : "ghost"}
|
||
onClick={handleSwimlanesToggle}
|
||
className="flex items-center gap-2"
|
||
title="Mode d'affichage"
|
||
>
|
||
<svg
|
||
className="w-4 h-4"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
{filters.swimlanesByTags ? (
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
||
) : (
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||
)}
|
||
</svg>
|
||
{!filters.swimlanesByTags
|
||
? 'Normal'
|
||
: filters.swimlanesMode === 'priority'
|
||
? 'Par priorité'
|
||
: 'Par tags'
|
||
}
|
||
</Button>
|
||
|
||
{/* Bouton pour changer le mode des swimlanes */}
|
||
{filters.swimlanesByTags && (
|
||
<Button
|
||
variant="ghost"
|
||
onClick={handleSwimlaneModeToggle}
|
||
className="flex items-center gap-1 px-2"
|
||
>
|
||
<svg
|
||
className={`w-3 h-3 transition-transform ${isSwimlaneModeExpanded ? 'rotate-180' : ''}`}
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||
</svg>
|
||
</Button>
|
||
)}
|
||
</div>
|
||
|
||
|
||
{/* Bouton de tri */}
|
||
<div className="relative" ref={sortDropdownRef}>
|
||
<Button
|
||
ref={sortButtonRef}
|
||
variant="ghost"
|
||
onClick={handleSortToggle}
|
||
className="flex items-center gap-2"
|
||
>
|
||
<svg
|
||
className="w-4 h-4"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" />
|
||
</svg>
|
||
Tris
|
||
<svg
|
||
className={`w-4 h-4 transition-transform ${isSortExpanded ? 'rotate-180' : ''}`}
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||
</svg>
|
||
</Button>
|
||
|
||
</div>
|
||
|
||
<Button
|
||
variant="ghost"
|
||
onClick={() => setIsExpanded(!isExpanded)}
|
||
className="flex items-center gap-2"
|
||
>
|
||
<svg
|
||
className="w-4 h-4"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||
</svg>
|
||
Filtres
|
||
{activeFiltersCount > 0 && (
|
||
<span className="bg-[var(--primary)] text-[var(--primary-foreground)] text-xs px-2 py-0.5 rounded-full font-medium">
|
||
{activeFiltersCount}
|
||
</span>
|
||
)}
|
||
<svg
|
||
className={`w-4 h-4 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||
</svg>
|
||
</Button>
|
||
|
||
{activeFiltersCount > 0 && (
|
||
<Button
|
||
variant="ghost"
|
||
onClick={handleClearFilters}
|
||
className="text-[var(--muted-foreground)] hover:text-[var(--destructive)]"
|
||
>
|
||
Effacer
|
||
</Button>
|
||
)}
|
||
</div>
|
||
|
||
{/* Filtres étendus */}
|
||
{isExpanded && (
|
||
<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="grid grid-cols-2 gap-2">
|
||
{priorityOptions.filter(priority => priority.count > 0).map((priority) => (
|
||
<button
|
||
key={priority.value}
|
||
onClick={() => handlePriorityToggle(priority.value)}
|
||
className={`flex items-center gap-2 px-3 py-2 rounded-lg border transition-all text-xs font-medium whitespace-nowrap ${
|
||
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>
|
||
</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-2 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-3 py-2 rounded-lg 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>
|
||
|
||
{/* 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>
|
||
|
||
{/* 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>
|
||
)}
|
||
|
||
{/* Visibilité des colonnes */}
|
||
<div className="col-span-full border-t border-[var(--border)]/50 pt-6 mt-4">
|
||
<ColumnVisibilityToggle
|
||
hiddenStatuses={hiddenStatuses}
|
||
onToggleStatus={toggleStatusVisibility}
|
||
tasks={regularTasks}
|
||
className="text-xs"
|
||
/>
|
||
</div>
|
||
|
||
{/* Résumé des filtres actifs */}
|
||
{activeFiltersCount > 0 && (
|
||
<div className="bg-[var(--card)]/30 rounded-lg p-3 border border-[var(--border)]/50 mt-4">
|
||
<div className="text-xs text-[var(--muted-foreground)] font-mono uppercase tracking-wider mb-2">
|
||
Filtres actifs
|
||
</div>
|
||
<div className="space-y-1 text-xs">
|
||
{filters.search && (
|
||
<div className="text-[var(--muted-foreground)]">
|
||
Recherche: <span className="text-cyan-400">“{filters.search}”</span>
|
||
</div>
|
||
)}
|
||
{(filters.priorities?.filter(Boolean).length || 0) > 0 && (
|
||
<div className="text-[var(--muted-foreground)]">
|
||
Priorités: <span className="text-cyan-400">{filters.priorities?.filter(Boolean).join(', ')}</span>
|
||
</div>
|
||
)}
|
||
{(filters.tags?.filter(Boolean).length || 0) > 0 && (
|
||
<div className="text-[var(--muted-foreground)]">
|
||
Tags: <span className="text-cyan-400">{filters.tags?.filter(Boolean).join(', ')}</span>
|
||
</div>
|
||
)}
|
||
{filters.showJiraOnly && (
|
||
<div className="text-[var(--muted-foreground)]">
|
||
Affichage: <span className="text-blue-400">Jira seulement</span>
|
||
</div>
|
||
)}
|
||
{filters.hideJiraTasks && (
|
||
<div className="text-[var(--muted-foreground)]">
|
||
Affichage: <span className="text-red-400">Masquer Jira</span>
|
||
</div>
|
||
)}
|
||
{(filters.jiraProjects?.filter(Boolean).length || 0) > 0 && (
|
||
<div className="text-[var(--muted-foreground)]">
|
||
Projets Jira: <span className="text-blue-400">{filters.jiraProjects?.filter(Boolean).join(', ')}</span>
|
||
</div>
|
||
)}
|
||
{(filters.jiraTypes?.filter(Boolean).length || 0) > 0 && (
|
||
<div className="text-[var(--muted-foreground)]">
|
||
Types Jira: <span className="text-purple-400">{filters.jiraTypes?.filter(Boolean).join(', ')}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Dropdown de tri rendu via portail pour éviter les problèmes de z-index */}
|
||
{isSortExpanded && typeof window !== 'undefined' && createPortal(
|
||
<div
|
||
ref={sortDropdownRef}
|
||
className="fixed w-80 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-xl z-[9999] max-h-64 overflow-y-auto"
|
||
style={{
|
||
top: dropdownPosition.top,
|
||
left: dropdownPosition.left
|
||
}}
|
||
>
|
||
{SORT_OPTIONS.map((option) => (
|
||
<button
|
||
key={option.key}
|
||
onClick={() => {
|
||
handleSortChange(option.key);
|
||
setIsSortExpanded(false);
|
||
}}
|
||
className={`w-full px-3 py-2 text-left text-xs font-mono hover:bg-[var(--card-hover)] transition-colors flex items-center gap-2 ${
|
||
(filters.sortBy || 'priority-desc') === option.key
|
||
? 'bg-cyan-600/20 text-cyan-400 border-l-2 border-cyan-400'
|
||
: 'text-[var(--muted-foreground)]'
|
||
}`}
|
||
>
|
||
<span className="text-base">{option.icon}</span>
|
||
<span className="flex-1">{option.label}</span>
|
||
{(filters.sortBy || 'priority-desc') === option.key && (
|
||
<svg className="w-4 h-4 text-cyan-400" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||
</svg>
|
||
)}
|
||
</button>
|
||
))}
|
||
</div>,
|
||
document.body
|
||
)}
|
||
|
||
{/* Dropdown des modes swimlanes rendu via portail pour éviter les problèmes de z-index */}
|
||
{isSwimlaneModeExpanded && typeof window !== 'undefined' && createPortal(
|
||
<div
|
||
ref={swimlaneModeDropdownRef}
|
||
className="fixed bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-xl z-[9999] min-w-[140px]"
|
||
style={{
|
||
top: dropdownPosition.top,
|
||
left: dropdownPosition.left,
|
||
}}
|
||
>
|
||
<button
|
||
onClick={() => handleSwimlaneModeChange('tags')}
|
||
className={`w-full px-3 py-2 text-left text-xs hover:bg-[var(--card-hover)] transition-colors flex items-center gap-2 first:rounded-t-lg ${
|
||
(!filters.swimlanesMode || filters.swimlanesMode === 'tags') ? 'bg-[var(--card-hover)] text-[var(--primary)]' : 'text-[var(--muted-foreground)]'
|
||
}`}
|
||
>
|
||
🏷️ Par tags
|
||
</button>
|
||
<button
|
||
onClick={() => handleSwimlaneModeChange('priority')}
|
||
className={`w-full px-3 py-2 text-left text-xs hover:bg-[var(--card-hover)] transition-colors flex items-center gap-2 last:rounded-b-lg ${
|
||
filters.swimlanesMode === 'priority' ? 'bg-[var(--card-hover)] text-[var(--primary)]' : 'text-[var(--muted-foreground)]'
|
||
}`}
|
||
>
|
||
🎯 Par priorité
|
||
</button>
|
||
</div>,
|
||
document.body
|
||
)}
|
||
</div>
|
||
);
|
||
}
|