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:
@@ -18,6 +18,10 @@ externalPort = 80
|
||||
localPort = 35345
|
||||
externalPort = 3002
|
||||
|
||||
[[ports]]
|
||||
localPort = 40247
|
||||
externalPort = 3001
|
||||
|
||||
[[ports]]
|
||||
localPort = 41353
|
||||
externalPort = 3000
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user