feat: add column visibility toggle to Kanban and Swimlanes boards
- Integrated `useColumnVisibility` hook for managing column visibility states. - Added `ColumnVisibilityToggle` component to both `KanbanBoard` and `SwimlanesBoard` for user control over visible columns. - Updated rendering logic to filter and display only visible columns, enhancing user experience and task organization.
This commit is contained in:
@@ -6,6 +6,8 @@ import { Button } from '@/components/ui/Button';
|
|||||||
import { CreateTaskForm } from '@/components/forms/CreateTaskForm';
|
import { CreateTaskForm } from '@/components/forms/CreateTaskForm';
|
||||||
import { CreateTaskData } from '@/clients/tasks-client';
|
import { CreateTaskData } from '@/clients/tasks-client';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
import { useColumnVisibility } from '@/hooks/useColumnVisibility';
|
||||||
|
import { ColumnVisibilityToggle } from './ColumnVisibilityToggle';
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
@@ -32,6 +34,9 @@ export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onU
|
|||||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
const [activeTask, setActiveTask] = useState<Task | null>(null);
|
const [activeTask, setActiveTask] = useState<Task | null>(null);
|
||||||
|
|
||||||
|
// Gestion de la visibilité des colonnes
|
||||||
|
const { hiddenStatuses, toggleStatusVisibility, getVisibleStatuses } = useColumnVisibility();
|
||||||
|
|
||||||
// Configuration des capteurs pour le drag & drop
|
// Configuration des capteurs pour le drag & drop
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
@@ -54,7 +59,7 @@ export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onU
|
|||||||
}, [tasks]);
|
}, [tasks]);
|
||||||
|
|
||||||
// Configuration des colonnes
|
// Configuration des colonnes
|
||||||
const columns: Array<{
|
const allColumns: Array<{
|
||||||
id: TaskStatus;
|
id: TaskStatus;
|
||||||
title: string;
|
title: string;
|
||||||
color: string;
|
color: string;
|
||||||
@@ -86,6 +91,9 @@ export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onU
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Filtrer les colonnes visibles
|
||||||
|
const visibleColumns = getVisibleStatuses(allColumns);
|
||||||
|
|
||||||
const handleCreateTask = async (data: CreateTaskData) => {
|
const handleCreateTask = async (data: CreateTaskData) => {
|
||||||
if (onCreateTask) {
|
if (onCreateTask) {
|
||||||
await onCreateTask(data);
|
await onCreateTask(data);
|
||||||
@@ -145,9 +153,18 @@ export function KanbanBoard({ tasks, onCreateTask, onDeleteTask, onEditTask, onU
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Toggle de visibilité des colonnes */}
|
||||||
|
<div className="px-6 pb-4">
|
||||||
|
<ColumnVisibilityToggle
|
||||||
|
statuses={allColumns}
|
||||||
|
hiddenStatuses={hiddenStatuses}
|
||||||
|
onToggleStatus={toggleStatusVisibility}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Board tech dark */}
|
{/* Board tech dark */}
|
||||||
<div className="flex-1 flex gap-6 overflow-x-auto p-6">
|
<div className="flex-1 flex gap-6 overflow-x-auto p-6">
|
||||||
{columns.map((column) => (
|
{visibleColumns.map((column) => (
|
||||||
<KanbanColumn
|
<KanbanColumn
|
||||||
key={column.id}
|
key={column.id}
|
||||||
id={column.id}
|
id={column.id}
|
||||||
|
|||||||
39
components/kanban/ColumnVisibilityToggle.tsx
Normal file
39
components/kanban/ColumnVisibilityToggle.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { TaskStatus } from '@/lib/types';
|
||||||
|
|
||||||
|
interface ColumnVisibilityToggleProps {
|
||||||
|
statuses: { id: TaskStatus; title: string; color: string }[];
|
||||||
|
hiddenStatuses: Set<TaskStatus>;
|
||||||
|
onToggleStatus: (status: TaskStatus) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ColumnVisibilityToggle({
|
||||||
|
statuses,
|
||||||
|
hiddenStatuses,
|
||||||
|
onToggleStatus,
|
||||||
|
className = ""
|
||||||
|
}: ColumnVisibilityToggleProps) {
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center gap-2 ${className}`}>
|
||||||
|
<span className="text-sm font-mono font-medium text-slate-400">
|
||||||
|
Colonnes :
|
||||||
|
</span>
|
||||||
|
{statuses.map(status => (
|
||||||
|
<button
|
||||||
|
key={status.id}
|
||||||
|
onClick={() => onToggleStatus(status.id)}
|
||||||
|
className={`px-3 py-1 rounded-lg text-xs font-mono font-medium transition-colors ${
|
||||||
|
hiddenStatuses.has(status.id)
|
||||||
|
? 'bg-slate-700/50 text-slate-500 hover:bg-slate-700'
|
||||||
|
: 'bg-blue-600/20 text-blue-300 border border-blue-500/30 hover:bg-blue-600/30'
|
||||||
|
}`}
|
||||||
|
title={hiddenStatuses.has(status.id) ? `Afficher ${status.title}` : `Masquer ${status.title}`}
|
||||||
|
>
|
||||||
|
{hiddenStatuses.has(status.id) ? '👁️🗨️' : '👁️'} {status.title}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import { Task, TaskStatus } from '@/lib/types';
|
|||||||
import { TaskCard } from './TaskCard';
|
import { TaskCard } from './TaskCard';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useTasksContext } from '@/contexts/TasksContext';
|
import { useTasksContext } from '@/contexts/TasksContext';
|
||||||
|
import { useColumnVisibility } from '@/hooks/useColumnVisibility';
|
||||||
|
import { ColumnVisibilityToggle } from './ColumnVisibilityToggle';
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
@@ -87,6 +89,9 @@ export function SwimlanesBoard({
|
|||||||
const [activeTask, setActiveTask] = useState<Task | null>(null);
|
const [activeTask, setActiveTask] = useState<Task | null>(null);
|
||||||
const [collapsedSwimlanes, setCollapsedSwimlanes] = useState<Set<string>>(new Set());
|
const [collapsedSwimlanes, setCollapsedSwimlanes] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// Gestion de la visibilité des colonnes
|
||||||
|
const { hiddenStatuses, toggleStatusVisibility, getVisibleStatuses } = useColumnVisibility();
|
||||||
|
|
||||||
// Configuration des capteurs pour le drag & drop
|
// Configuration des capteurs pour le drag & drop
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
@@ -137,6 +142,9 @@ export function SwimlanesBoard({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Filtrer les statuts visibles
|
||||||
|
const visibleStatuses = getVisibleStatuses(statuses);
|
||||||
|
|
||||||
// Grouper les tâches par tags
|
// Grouper les tâches par tags
|
||||||
const tasksByTag = useMemo(() => {
|
const tasksByTag = useMemo(() => {
|
||||||
const grouped: { [tagName: string]: Task[] } = {};
|
const grouped: { [tagName: string]: Task[] } = {};
|
||||||
@@ -184,9 +192,21 @@ export function SwimlanesBoard({
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Headers des colonnes */}
|
{/* Headers des colonnes avec boutons toggle */}
|
||||||
<div className="grid grid-cols-4 gap-4 px-6 pb-4 ml-8">
|
<div className="flex items-center justify-between px-6 pb-4">
|
||||||
{statuses.map(status => (
|
<ColumnVisibilityToggle
|
||||||
|
statuses={statuses}
|
||||||
|
hiddenStatuses={hiddenStatuses}
|
||||||
|
onToggleStatus={toggleStatusVisibility}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Headers des colonnes visibles */}
|
||||||
|
<div
|
||||||
|
className={`grid gap-4 px-6 pb-4 ml-8`}
|
||||||
|
style={{ gridTemplateColumns: `repeat(${visibleStatuses.length}, 1fr)` }}
|
||||||
|
>
|
||||||
|
{visibleStatuses.map(status => (
|
||||||
<div key={status.id} className="text-center">
|
<div key={status.id} className="text-center">
|
||||||
<div className="text-sm font-mono font-bold text-slate-300 uppercase tracking-wider">
|
<div className="text-sm font-mono font-bold text-slate-300 uppercase tracking-wider">
|
||||||
{status.title}
|
{status.title}
|
||||||
@@ -238,8 +258,11 @@ export function SwimlanesBoard({
|
|||||||
|
|
||||||
{/* Contenu de la swimlane */}
|
{/* Contenu de la swimlane */}
|
||||||
{!collapsedSwimlanes.has(tagName) && (
|
{!collapsedSwimlanes.has(tagName) && (
|
||||||
<div className="grid grid-cols-4 gap-4 p-2 ml-8">
|
<div
|
||||||
{statuses.map(status => {
|
className="gap-4 p-2 ml-8 grid"
|
||||||
|
style={{ gridTemplateColumns: `repeat(${visibleStatuses.length}, 1fr)` }}
|
||||||
|
>
|
||||||
|
{visibleStatuses.map(status => {
|
||||||
const statusTasks = tagTasks.filter(task => task.status === status.id);
|
const statusTasks = tagTasks.filter(task => task.status === status.id);
|
||||||
return (
|
return (
|
||||||
<DroppableColumn
|
<DroppableColumn
|
||||||
|
|||||||
35
hooks/useColumnVisibility.ts
Normal file
35
hooks/useColumnVisibility.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { TaskStatus } from '@/lib/types';
|
||||||
|
|
||||||
|
export function useColumnVisibility(initialHidden: TaskStatus[] = []) {
|
||||||
|
const [hiddenStatuses, setHiddenStatuses] = useState<Set<TaskStatus>>(
|
||||||
|
new Set(initialHidden)
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleStatusVisibility = (status: TaskStatus) => {
|
||||||
|
setHiddenStatuses(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(status)) {
|
||||||
|
newSet.delete(status);
|
||||||
|
} else {
|
||||||
|
newSet.add(status);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVisibleStatuses = <T extends { id: TaskStatus }>(statuses: T[]): T[] => {
|
||||||
|
return statuses.filter(status => !hiddenStatuses.has(status.id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isStatusVisible = (status: TaskStatus): boolean => {
|
||||||
|
return !hiddenStatuses.has(status);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
hiddenStatuses,
|
||||||
|
toggleStatusVisibility,
|
||||||
|
getVisibleStatuses,
|
||||||
|
isStatusVisible
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user