feat: add swimlane mode selection to KanbanFilters and BoardContainer

- Introduced `swimlanesMode` in `KanbanFilters` to toggle between 'tags' and 'priority' swimlanes.
- Updated `KanbanBoardContainer` to conditionally render `PrioritySwimlanesBoard` based on the selected mode.
- Enhanced UI to include dropdown for swimlane mode selection, improving user experience in task organization.
- Adjusted `TasksContext` to persist swimlane mode preferences, ensuring consistent behavior across sessions.
This commit is contained in:
Julien Froidefond
2025-09-15 10:17:36 +02:00
parent 631da57ea2
commit 05cd099cf4
7 changed files with 495 additions and 288 deletions

View File

@@ -16,6 +16,7 @@ export interface KanbanFilters {
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
}
@@ -29,23 +30,28 @@ export function KanbanFilters({ filters, onFiltersChange }: KanbanFiltersProps)
const { tags: availableTags, regularTasks } = useTasksContext();
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 le dropdown de tri en cliquant à l'extérieur
// 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) {
if (isSortExpanded || isSwimlaneModeExpanded) {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}
}, [isSortExpanded]);
}, [isSortExpanded, isSwimlaneModeExpanded]);
const handleSearchChange = (search: string) => {
onFiltersChange({ ...filters, search: search || undefined });
@@ -89,6 +95,25 @@ export function KanbanFilters({ filters, onFiltersChange }: KanbanFiltersProps)
});
};
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,
@@ -162,27 +187,52 @@ export function KanbanFilters({ filters, onFiltersChange }: KanbanFiltersProps)
/>
</div>
{/* Bouton swimlanes par tags */}
<Button
variant={filters.swimlanesByTags ? "primary" : "ghost"}
onClick={handleSwimlanesToggle}
className="flex items-center gap-2"
title={filters.swimlanesByTags ? "Vue normale" : "Lignes par tags"}
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
{/* 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"
>
{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' : 'Swimlanes'}
</Button>
<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 vue compacte */}
<Button
@@ -399,6 +449,36 @@ export function KanbanFilters({ filters, onFiltersChange }: KanbanFiltersProps)
</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-slate-800 border border-slate-700 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-slate-700 transition-colors flex items-center gap-2 first:rounded-t-lg ${
(!filters.swimlanesMode || filters.swimlanesMode === 'tags') ? 'bg-slate-700 text-cyan-400' : 'text-slate-300'
}`}
>
🏷 Par tags
</button>
<button
onClick={() => handleSwimlaneModeChange('priority')}
className={`w-full px-3 py-2 text-left text-xs hover:bg-slate-700 transition-colors flex items-center gap-2 last:rounded-b-lg ${
filters.swimlanesMode === 'priority' ? 'bg-slate-700 text-cyan-400' : 'text-slate-300'
}`}
>
🎯 Par priorité
</button>
</div>,
document.body
)}
</div>
);
}