Files
towercontrol/components/kanban/SwimlanesBase.tsx
Julien Froidefond 1a21f9b88b feat: enhance Kanban components with visibleStatuses prop
- Added `visibleStatuses` prop to `KanbanBoard`, `PrioritySwimlanesBoard`, `SwimlanesBase`, and `SwimlanesBoard` for improved column visibility control.
- Updated `KanbanBoardContainer` to derive `visibleStatuses` from `useColumnVisibility`, allowing dynamic filtering of displayed statuses.
- Refactored `KanbanFilters` to accept `hiddenStatuses` and `onToggleStatusVisibility` props, enabling better integration with column visibility management.
- Cleaned up visibility logic across components to ensure consistent behavior and user experience.
2025-09-15 11:05:11 +02:00

258 lines
7.9 KiB
TypeScript

'use client';
import { Task, TaskStatus } from '@/lib/types';
import { TaskCard } from './TaskCard';
import { useState } from 'react';
import { useColumnVisibility } from '@/hooks/useColumnVisibility';
import { getAllStatuses } from '@/lib/status-config';
import {
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
closestCenter,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
SortableContext,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { useDroppable } from '@dnd-kit/core';
// Composant pour les colonnes droppables
function DroppableColumn({
status,
tasks,
onDeleteTask,
onEditTask,
onUpdateTitle,
compactView
}: {
status: TaskStatus;
tasks: Task[];
onDeleteTask?: (taskId: string) => Promise<void>;
onEditTask?: (task: Task) => void;
onUpdateTitle?: (taskId: string, newTitle: string) => Promise<void>;
compactView: boolean;
}) {
const { setNodeRef } = useDroppable({
id: status,
});
return (
<div ref={setNodeRef} className="min-h-[100px]">
<SortableContext items={tasks.map(t => t.id)} strategy={verticalListSortingStrategy}>
<div className="space-y-1">
{tasks.map(task => (
<TaskCard
key={task.id}
task={task}
onDelete={onDeleteTask}
onEdit={onEditTask}
onUpdateTitle={onUpdateTitle}
compactView={compactView}
/>
))}
</div>
</SortableContext>
</div>
);
}
// Interface pour une swimlane
export interface SwimlaneData {
key: string;
label: string;
icon?: string;
color?: string;
tasks: Task[];
}
interface SwimlanesBaseProps {
tasks: Task[];
title: string;
swimlanes: SwimlaneData[];
onDeleteTask?: (taskId: string) => Promise<void>;
onEditTask?: (task: Task) => void;
onUpdateTitle?: (taskId: string, newTitle: string) => Promise<void>;
onUpdateStatus?: (taskId: string, newStatus: TaskStatus) => Promise<void>;
compactView?: boolean;
visibleStatuses?: TaskStatus[];
}
export function SwimlanesBase({
tasks,
title,
swimlanes,
onDeleteTask,
onEditTask,
onUpdateTitle,
onUpdateStatus,
compactView = false,
visibleStatuses
}: SwimlanesBaseProps) {
const [activeTask, setActiveTask] = useState<Task | null>(null);
const [collapsedSwimlanes, setCollapsedSwimlanes] = useState<Set<string>>(new Set());
// Gestion de la visibilité des colonnes
const { getVisibleStatuses } = useColumnVisibility();
const allStatuses = getAllStatuses();
const statusesToShow = visibleStatuses ||
getVisibleStatuses(allStatuses.map(s => ({ id: s.key }))).map(s => s.id);
// Configuration des sensors pour le drag & drop
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
})
);
// Handlers pour le drag & drop
const handleDragStart = (event: DragStartEvent) => {
const task = tasks.find(t => t.id === event.active.id);
setActiveTask(task || null);
};
const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
setActiveTask(null);
if (!over || !onUpdateStatus) return;
const taskId = active.id as string;
const newStatus = over.id as TaskStatus;
// Vérifier si le statut a changé
const task = tasks.find(t => t.id === taskId);
if (task && task.status !== newStatus) {
await onUpdateStatus(taskId, newStatus);
}
};
// Basculer l'état d'une swimlane
const toggleSwimlane = (swimlaneKey: string) => {
const newCollapsed = new Set(collapsedSwimlanes);
if (newCollapsed.has(swimlaneKey)) {
newCollapsed.delete(swimlaneKey);
} else {
newCollapsed.add(swimlaneKey);
}
setCollapsedSwimlanes(newCollapsed);
};
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="flex flex-col h-full bg-slate-950">
{/* Header */}
<div className="flex-shrink-0 px-6 py-4 border-b border-slate-700/50">
<h2 className="text-lg font-mono font-bold text-slate-200 uppercase tracking-wider">
{title}
</h2>
</div>
{/* Headers des colonnes visibles */}
<div
className={`grid gap-4 px-6 py-4 ml-8`}
style={{ gridTemplateColumns: `repeat(${statusesToShow.length}, minmax(0, 1fr))` }}
>
{statusesToShow.map(status => {
const statusConfig = allStatuses.find(s => s.key === status);
return (
<div key={status} className="text-center">
<h3 className="text-sm font-mono font-bold text-slate-300 uppercase tracking-wider">
{statusConfig?.icon} {statusConfig?.label}
</h3>
</div>
);
})}
</div>
{/* Swimlanes */}
<div className="flex-1 overflow-y-auto px-6">
<div className="space-y-4">
{swimlanes.map(swimlane => {
const isCollapsed = collapsedSwimlanes.has(swimlane.key);
return (
<div key={swimlane.key} className="border border-slate-700/50 rounded-lg bg-slate-900/30">
{/* Header de la swimlane */}
<div className="flex items-center p-2 border-b border-slate-700/50">
<button
onClick={() => toggleSwimlane(swimlane.key)}
className="flex items-center gap-2 hover:bg-slate-800/50 rounded p-1 -m-1 transition-colors"
>
<svg
className={`w-4 h-4 text-slate-400 transition-transform ${isCollapsed ? '' : 'rotate-90'}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
{swimlane.color && (
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: swimlane.color }}
/>
)}
<span className="text-slate-200 font-medium text-sm">
{swimlane.icon && `${swimlane.icon} `}
{swimlane.label} ({swimlane.tasks.length})
</span>
</button>
</div>
{/* Contenu de la swimlane */}
{!isCollapsed && (
<div
className={`grid gap-4 p-4`}
style={{ gridTemplateColumns: `repeat(${statusesToShow.length}, minmax(0, 1fr))` }}
>
{statusesToShow.map(status => {
const statusTasks = swimlane.tasks.filter(task => task.status === status);
return (
<DroppableColumn
key={`${swimlane.key}-${status}`}
status={status}
tasks={statusTasks}
onDeleteTask={onDeleteTask}
onEditTask={onEditTask}
onUpdateTitle={onUpdateTitle}
compactView={compactView}
/>
);
})}
</div>
)}
</div>
);
})}
</div>
</div>
</div>
{/* Drag overlay */}
<DragOverlay>
{activeTask && (
<TaskCard
task={activeTask}
compactView={compactView}
/>
)}
</DragOverlay>
</DndContext>
);
}