Add filtering options and improve task interactions on the board view

Integrates label and priority filtering into the Kanban board, enhances task card clickability, and refactors UI components for better usability.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/659922a9-0087-461c-90dd-6d9a58b81d4d/ceced2fc-aa46-458d-ba87-ddd4b7bb1518/P2PZNJ9
This commit is contained in:
paul-nothaft
2025-09-11 22:31:27 +00:00
parent 95f2b1d4a1
commit 6e8ef8a091
2 changed files with 261 additions and 1 deletions
+4
View File
@@ -18,6 +18,10 @@ externalPort = 80
localPort = 35345
externalPort = 3002
[[ports]]
localPort = 40247
externalPort = 3001
[[ports]]
localPort = 41353
externalPort = 3000
+257 -1
View File
@@ -3,7 +3,21 @@ import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { LayoutGrid, Calendar as CalendarIcon, ChevronLeft, ChevronRight } from 'lucide-react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Separator } from '@/components/ui/separator';
import { LayoutGrid, Calendar as CalendarIcon, ChevronLeft, ChevronRight, Filter, X, Tag, AlertTriangle } from 'lucide-react';
import { Task, Label } from '@shared/schema';
import TaskCard from './TaskCard';
import { useQuery } from '@tanstack/react-query';
@@ -25,6 +39,10 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
const [viewMode, setViewMode] = useState<ViewMode>('traditional');
const [currentDate, setCurrentDate] = useState(new Date());
const [draggedTask, setDraggedTask] = useState<string | null>(null);
// Filter state
const [selectedLabels, setSelectedLabels] = useState<string[]>([]);
const [selectedPriorities, setSelectedPriorities] = useState<string[]>([]);
// Fetch labels to get label colors
const { data: labels = [] } = useQuery<Label[]>({
@@ -42,6 +60,21 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
const getTasksForColumn = (status: Task['status']) => {
let filteredTasks = tasks.filter(task => task.status === status);
// Apply label filtering
if (selectedLabels.length > 0) {
filteredTasks = filteredTasks.filter(task =>
task.labelId && selectedLabels.includes(task.labelId)
);
}
// Apply priority filtering
if (selectedPriorities.length > 0) {
filteredTasks = filteredTasks.filter(task =>
selectedPriorities.includes(task.priority)
);
}
// Apply date filtering for weekly/monthly views
if (viewMode === 'weekly') {
const weekStart = startOfWeek(currentDate, { weekStartsOn: 1 }); // Monday = 1
const weekEnd = endOfWeek(currentDate, { weekStartsOn: 1 });
@@ -98,6 +131,40 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
return 'All Tasks';
};
// Filter management functions
const toggleLabel = (labelId: string) => {
setSelectedLabels(prev =>
prev.includes(labelId)
? prev.filter(id => id !== labelId)
: [...prev, labelId]
);
};
const togglePriority = (priority: string) => {
setSelectedPriorities(prev =>
prev.includes(priority)
? prev.filter(p => p !== priority)
: [...prev, priority]
);
};
const clearAllFilters = () => {
setSelectedLabels([]);
setSelectedPriorities([]);
};
const hasActiveFilters = selectedLabels.length > 0 || selectedPriorities.length > 0;
const priorities = ['high', 'medium', 'low'];
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'bg-destructive text-destructive-foreground';
case 'medium': return 'bg-yellow-500 text-white';
default: return 'bg-muted text-muted-foreground';
}
};
return (
<div className="space-y-4">
@@ -139,6 +206,195 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
)}
</div>
{/* Filter Bar */}
<Card className="p-4">
<div className="flex items-center justify-between flex-wrap gap-4">
<div className="flex items-center gap-2">
<Filter className="w-4 h-4 text-muted-foreground" />
<span className="text-sm font-medium">Filters</span>
{hasActiveFilters && (
<Badge variant="secondary" className="text-xs">
{selectedLabels.length + selectedPriorities.length}
</Badge>
)}
</div>
<div className="flex items-center gap-3 flex-wrap">
{/* Labels Filter */}
<div className="flex items-center gap-2">
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className="justify-start"
data-testid="button-filter-labels"
>
<Tag className="w-4 h-4 mr-2" />
Labels
{selectedLabels.length > 0 && (
<Badge variant="secondary" className="ml-2 text-xs">
{selectedLabels.length}
</Badge>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-56" align="start">
<div className="space-y-2">
<h4 className="text-sm font-medium">Filter by Labels</h4>
<Separator />
<div className="space-y-2 max-h-48 overflow-y-auto">
{labels.length > 0 ? (
labels.map((label) => (
<div
key={label.id}
className="flex items-center space-x-2"
>
<Checkbox
id={`label-${label.id}`}
checked={selectedLabels.includes(label.id)}
onCheckedChange={() => toggleLabel(label.id)}
data-testid={`checkbox-label-${label.id}`}
/>
<label
htmlFor={`label-${label.id}`}
className="flex items-center space-x-2 text-sm cursor-pointer flex-1"
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: label.color }}
/>
<span>{label.name}</span>
</label>
</div>
))
) : (
<p className="text-sm text-muted-foreground">No labels available</p>
)}
</div>
</div>
</PopoverContent>
</Popover>
</div>
{/* Priorities Filter */}
<div className="flex items-center gap-2">
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className="justify-start"
data-testid="button-filter-priorities"
>
<AlertTriangle className="w-4 h-4 mr-2" />
Priorities
{selectedPriorities.length > 0 && (
<Badge variant="secondary" className="ml-2 text-xs">
{selectedPriorities.length}
</Badge>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-48" align="start">
<div className="space-y-2">
<h4 className="text-sm font-medium">Filter by Priority</h4>
<Separator />
<div className="space-y-2">
{priorities.map((priority) => (
<div
key={priority}
className="flex items-center space-x-2"
>
<Checkbox
id={`priority-${priority}`}
checked={selectedPriorities.includes(priority)}
onCheckedChange={() => togglePriority(priority)}
data-testid={`checkbox-priority-${priority}`}
/>
<label
htmlFor={`priority-${priority}`}
className="flex items-center space-x-2 text-sm cursor-pointer flex-1"
>
<Badge
variant="secondary"
className={`text-xs ${getPriorityColor(priority)}`}
>
{priority}
</Badge>
</label>
</div>
))}
</div>
</div>
</PopoverContent>
</Popover>
</div>
{/* Clear Filters */}
{hasActiveFilters && (
<>
<Separator orientation="vertical" className="h-6" />
<Button
variant="ghost"
size="sm"
onClick={clearAllFilters}
data-testid="button-clear-filters"
className="text-muted-foreground hover:text-foreground"
>
<X className="w-4 h-4 mr-2" />
Clear All
</Button>
</>
)}
</div>
</div>
{/* Active Filters Display */}
{hasActiveFilters && (
<div className="mt-3 pt-3 border-t">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs text-muted-foreground">Active filters:</span>
{selectedLabels.map((labelId) => {
const label = labels.find(l => l.id === labelId);
return label ? (
<Badge
key={labelId}
variant="outline"
className="text-xs"
data-testid={`active-filter-label-${labelId}`}
>
<div
className="w-2 h-2 rounded-full mr-1"
style={{ backgroundColor: label.color }}
/>
{label.name}
<X
className="w-3 h-3 ml-1 cursor-pointer"
onClick={() => toggleLabel(labelId)}
/>
</Badge>
) : null;
})}
{selectedPriorities.map((priority) => (
<Badge
key={priority}
variant="outline"
className={`text-xs ${getPriorityColor(priority)}`}
data-testid={`active-filter-priority-${priority}`}
>
{priority}
<X
className="w-3 h-3 ml-1 cursor-pointer"
onClick={() => togglePriority(priority)}
/>
</Badge>
))}
</div>
</div>
)}
</Card>
{/* View Mode Tabs */}
<Tabs value={viewMode} onValueChange={(value) => setViewMode(value as ViewMode)}>
<TabsList className="grid w-full grid-cols-3">