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:
@@ -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">“{filters.search}”</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user