feat: add sorting functionality to KanbanFilters

- Enhanced `KanbanFilters` to include sorting options, allowing users to sort tasks dynamically.
- Implemented dropdown for sorting with options rendered via portal to manage z-index issues.
- Updated `TasksContext` to handle sorting preferences and apply sorting logic to both pinned and regular tasks.
- Added `sortBy` property to `KanbanFilters` and user preferences for persistent sorting settings.
This commit is contained in:
Julien Froidefond
2025-09-15 08:32:01 +02:00
parent d681a6c127
commit c8cacf1714
4 changed files with 357 additions and 17 deletions

View File

@@ -1,11 +1,13 @@
'use client';
import { useState } from 'react';
import { useState, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import { TaskPriority } 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';
export interface KanbanFilters {
search?: string;
@@ -15,6 +17,7 @@ export interface KanbanFilters {
compactView?: boolean;
swimlanesByTags?: boolean;
pinnedTag?: string; // Tag pour les objectifs principaux
sortBy?: string; // Clé de l'option de tri sélectionnée
}
interface KanbanFiltersProps {
@@ -25,6 +28,24 @@ interface KanbanFiltersProps {
export function KanbanFilters({ filters, onFiltersChange }: KanbanFiltersProps) {
const { tags: availableTags } = useTasksContext();
const [isExpanded, setIsExpanded] = useState(false);
const [isSortExpanded, setIsSortExpanded] = useState(false);
const sortDropdownRef = useRef<HTMLDivElement>(null);
const sortButtonRef = useRef<HTMLButtonElement>(null);
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
// Fermer le dropdown de tri en cliquant à l'extérieur
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (sortDropdownRef.current && !sortDropdownRef.current.contains(event.target as Node)) {
setIsSortExpanded(false);
}
}
if (isSortExpanded) {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}
}, [isSortExpanded]);
const handleSearchChange = (search: string) => {
onFiltersChange({ ...filters, search: search || undefined });
@@ -68,6 +89,24 @@ export function KanbanFilters({ filters, onFiltersChange }: KanbanFiltersProps)
});
};
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 handleClearFilters = () => {
onFiltersChange({});
};
@@ -139,11 +178,54 @@ export function KanbanFilters({ filters, onFiltersChange }: KanbanFiltersProps)
{filters.compactView ? 'Détaillée' : 'Compacte'}
</Button>
{/* 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-cyan-500 text-slate-900 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"
@@ -152,12 +234,6 @@ export function KanbanFilters({ filters, onFiltersChange }: KanbanFiltersProps)
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
Filtres
{activeFiltersCount > 0 && (
<span className="bg-cyan-500 text-slate-900 text-xs px-2 py-0.5 rounded-full font-medium">
{activeFiltersCount}
</span>
)}
</Button>
{activeFiltersCount > 0 && (
@@ -176,7 +252,7 @@ export function KanbanFilters({ filters, onFiltersChange }: KanbanFiltersProps)
<div className="mt-4 space-y-4 border-t border-slate-700/50 pt-4">
{/* Filtres par priorité */}
<div className="space-y-2">
<label className="block text-sm font-mono font-medium text-slate-300 uppercase tracking-wider">
<label className="block text-xs font-mono font-medium text-slate-300 uppercase tracking-wider">
Priorités
</label>
<div className="flex flex-wrap gap-2">
@@ -184,7 +260,7 @@ export function KanbanFilters({ filters, onFiltersChange }: KanbanFiltersProps)
<button
key={priority.value}
onClick={() => handlePriorityToggle(priority.value)}
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg border transition-all text-sm font-medium ${
className={`flex items-center gap-2 px-2 py-1 rounded-lg border transition-all text-xs font-medium ${
filters.priorities?.includes(priority.value)
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400'
: 'border-slate-600 bg-slate-800/50 text-slate-400 hover:border-slate-500'
@@ -203,7 +279,7 @@ export function KanbanFilters({ filters, onFiltersChange }: KanbanFiltersProps)
{/* Filtres par tags */}
{availableTags.length > 0 && (
<div className="space-y-2">
<label className="block text-sm font-mono font-medium text-slate-300 uppercase tracking-wider">
<label className="block text-xs font-mono font-medium text-slate-300 uppercase tracking-wider">
Tags
</label>
<div className="flex flex-wrap gap-2">
@@ -211,7 +287,7 @@ export function KanbanFilters({ filters, onFiltersChange }: KanbanFiltersProps)
<button
key={tag.id}
onClick={() => handleTagToggle(tag.name)}
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg border transition-all text-sm font-medium ${
className={`flex items-center gap-2 px-2 py-1 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-slate-600 bg-slate-800/50 text-slate-400 hover:border-slate-500'
@@ -234,7 +310,7 @@ export function KanbanFilters({ filters, onFiltersChange }: KanbanFiltersProps)
<div className="text-xs text-slate-400 font-mono uppercase tracking-wider mb-2">
Filtres actifs
</div>
<div className="space-y-1 text-sm">
<div className="space-y-1 text-xs">
{filters.search && (
<div className="text-slate-300">
Recherche: <span className="text-cyan-400">&ldquo;{filters.search}&rdquo;</span>
@@ -256,6 +332,42 @@ export function KanbanFilters({ filters, onFiltersChange }: KanbanFiltersProps)
</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-slate-800 border border-slate-700 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-slate-700 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-slate-300'
}`}
>
<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
)}
</div>
);
}