Skip to content

Commit 8714f5f

Browse files
committed
Add advanced features documentation and enhanced UI components guide
- Created ADVANCED_FEATURES.md detailing Sprint Planning, Advanced Filtering, and Epic Hierarchy components. - Added ENHANCED_COMPONENTS.md for IssueDetailModal component with features, props, and integration examples. - Introduced JIRA_TESTING_GUIDE.md for comprehensive testing steps and troubleshooting for Jira-like features.
1 parent 1678f3b commit 8714f5f

10 files changed

Lines changed: 3329 additions & 122 deletions

File tree

app/dashboard/projects/[id]/backlog/page.tsx

Lines changed: 241 additions & 51 deletions
Large diffs are not rendered by default.

app/dashboard/projects/[id]/kanban/page.tsx

Lines changed: 288 additions & 57 deletions
Large diffs are not rendered by default.

components/advanced-filters.tsx

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
"use client"
2+
3+
import { useState } from "react"
4+
import { Button } from "@/components/ui/button"
5+
import { Card } from "@/components/ui/card"
6+
import { Input } from "@/components/ui/input"
7+
import { Label } from "@/components/ui/label"
8+
import {
9+
X,
10+
Search,
11+
Filter,
12+
ChevronDown,
13+
Check,
14+
} from "lucide-react"
15+
import type { Task, Label as LabelType } from "@/lib/project-management"
16+
17+
export interface FilterOptions {
18+
search: string
19+
statuses: Task['status'][]
20+
priorities: Task['priority'][]
21+
assignees: string[]
22+
labels: string[]
23+
types: Array<'story' | 'task' | 'bug' | 'epic' | 'subtask'>
24+
}
25+
26+
interface AdvancedFiltersProps {
27+
filters: FilterOptions
28+
availableLabels: LabelType[]
29+
availableAssignees: string[]
30+
onFilterChange: (filters: FilterOptions) => void
31+
onClear: () => void
32+
}
33+
34+
const STATUS_OPTIONS: { value: Task['status']; label: string; color: string }[] = [
35+
{ value: 'backlog', label: 'Backlog', color: 'bg-gray-100 text-gray-800' },
36+
{ value: 'todo', label: 'To Do', color: 'bg-blue-100 text-blue-800' },
37+
{ value: 'in-progress', label: 'In Progress', color: 'bg-yellow-100 text-yellow-800' },
38+
{ value: 'review', label: 'Review', color: 'bg-purple-100 text-purple-800' },
39+
{ value: 'done', label: 'Done', color: 'bg-green-100 text-green-800' },
40+
{ value: 'closed', label: 'Closed', color: 'bg-gray-100 text-gray-800' },
41+
{ value: 'blocked', label: 'Blocked', color: 'bg-red-100 text-red-800' },
42+
]
43+
44+
const PRIORITY_OPTIONS: { value: Task['priority']; label: string; color: string }[] = [
45+
{ value: 'urgent', label: 'Urgent', color: 'text-red-500' },
46+
{ value: 'high', label: 'High', color: 'text-orange-500' },
47+
{ value: 'medium', label: 'Medium', color: 'text-yellow-500' },
48+
{ value: 'low', label: 'Low', color: 'text-green-500' },
49+
{ value: 'lowest', label: 'Lowest', color: 'text-gray-500' },
50+
]
51+
52+
const TYPE_OPTIONS: { value: 'story' | 'task' | 'bug' | 'epic' | 'subtask'; label: string }[] = [
53+
{ value: 'story', label: 'Story' },
54+
{ value: 'task', label: 'Task' },
55+
{ value: 'bug', label: 'Bug' },
56+
{ value: 'epic', label: 'Epic' },
57+
{ value: 'subtask', label: 'Subtask' },
58+
]
59+
60+
export function AdvancedFilters({
61+
filters,
62+
availableLabels,
63+
availableAssignees,
64+
onFilterChange,
65+
onClear,
66+
}: AdvancedFiltersProps) {
67+
const [expandedSection, setExpandedSection] = useState<string | null>(null)
68+
69+
const toggleSection = (section: string) => {
70+
setExpandedSection(expandedSection === section ? null : section)
71+
}
72+
73+
const handleSearchChange = (search: string) => {
74+
onFilterChange({ ...filters, search })
75+
}
76+
77+
const toggleStatus = (status: Task['status']) => {
78+
const newStatuses = filters.statuses.includes(status)
79+
? filters.statuses.filter(s => s !== status)
80+
: [...filters.statuses, status]
81+
onFilterChange({ ...filters, statuses: newStatuses })
82+
}
83+
84+
const togglePriority = (priority: Task['priority']) => {
85+
const newPriorities = filters.priorities.includes(priority)
86+
? filters.priorities.filter(p => p !== priority)
87+
: [...filters.priorities, priority]
88+
onFilterChange({ ...filters, priorities: newPriorities })
89+
}
90+
91+
const toggleAssignee = (assignee: string) => {
92+
const newAssignees = filters.assignees.includes(assignee)
93+
? filters.assignees.filter(a => a !== assignee)
94+
: [...filters.assignees, assignee]
95+
onFilterChange({ ...filters, assignees: newAssignees })
96+
}
97+
98+
const toggleLabel = (labelId: string) => {
99+
const newLabels = filters.labels.includes(labelId)
100+
? filters.labels.filter(l => l !== labelId)
101+
: [...filters.labels, labelId]
102+
onFilterChange({ ...filters, labels: newLabels })
103+
}
104+
105+
const toggleType = (type: 'story' | 'task' | 'bug' | 'epic' | 'subtask') => {
106+
const newTypes = filters.types.includes(type)
107+
? filters.types.filter(t => t !== type)
108+
: [...filters.types, type]
109+
onFilterChange({ ...filters, types: newTypes })
110+
}
111+
112+
const hasActiveFilters =
113+
filters.search !== '' ||
114+
filters.statuses.length > 0 ||
115+
filters.priorities.length > 0 ||
116+
filters.assignees.length > 0 ||
117+
filters.labels.length > 0 ||
118+
filters.types.length > 0
119+
120+
const activeFilterCount =
121+
(filters.search !== '' ? 1 : 0) +
122+
filters.statuses.length +
123+
filters.priorities.length +
124+
filters.assignees.length +
125+
filters.labels.length +
126+
filters.types.length
127+
128+
return (
129+
<Card className="p-4 space-y-4">
130+
{/* Header */}
131+
<div className="flex items-center justify-between">
132+
<div className="flex items-center gap-2">
133+
<Filter className="h-4 w-4" />
134+
<h3 className="font-semibold">Filters</h3>
135+
{hasActiveFilters && (
136+
<span className="px-2 py-0.5 text-xs bg-primary text-primary-foreground rounded-full">
137+
{activeFilterCount}
138+
</span>
139+
)}
140+
</div>
141+
{hasActiveFilters && (
142+
<Button variant="ghost" size="sm" onClick={onClear} className="h-7 text-xs">
143+
Clear All
144+
</Button>
145+
)}
146+
</div>
147+
148+
{/* Search */}
149+
<div className="relative">
150+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
151+
<Input
152+
type="text"
153+
placeholder="Search issues..."
154+
value={filters.search}
155+
onChange={(e) => handleSearchChange(e.target.value)}
156+
className="pl-9"
157+
/>
158+
</div>
159+
160+
{/* Status Filter */}
161+
<div className="border-t pt-4">
162+
<button
163+
onClick={() => toggleSection('status')}
164+
className="flex items-center justify-between w-full text-sm font-medium mb-2 hover:text-primary transition-colors"
165+
>
166+
<span>Status {filters.statuses.length > 0 && `(${filters.statuses.length})`}</span>
167+
<ChevronDown className={`h-4 w-4 transition-transform ${expandedSection === 'status' ? 'rotate-180' : ''}`} />
168+
</button>
169+
{expandedSection === 'status' && (
170+
<div className="space-y-1">
171+
{STATUS_OPTIONS.map((option) => (
172+
<label
173+
key={option.value}
174+
className="flex items-center gap-2 p-2 rounded hover:bg-muted cursor-pointer"
175+
>
176+
<input
177+
type="checkbox"
178+
checked={filters.statuses.includes(option.value)}
179+
onChange={() => toggleStatus(option.value)}
180+
className="rounded border-border"
181+
/>
182+
<span className={`px-2 py-0.5 rounded text-xs ${option.color}`}>
183+
{option.label}
184+
</span>
185+
</label>
186+
))}
187+
</div>
188+
)}
189+
</div>
190+
191+
{/* Priority Filter */}
192+
<div className="border-t pt-4">
193+
<button
194+
onClick={() => toggleSection('priority')}
195+
className="flex items-center justify-between w-full text-sm font-medium mb-2 hover:text-primary transition-colors"
196+
>
197+
<span>Priority {filters.priorities.length > 0 && `(${filters.priorities.length})`}</span>
198+
<ChevronDown className={`h-4 w-4 transition-transform ${expandedSection === 'priority' ? 'rotate-180' : ''}`} />
199+
</button>
200+
{expandedSection === 'priority' && (
201+
<div className="space-y-1">
202+
{PRIORITY_OPTIONS.map((option) => (
203+
<label
204+
key={option.value}
205+
className="flex items-center gap-2 p-2 rounded hover:bg-muted cursor-pointer"
206+
>
207+
<input
208+
type="checkbox"
209+
checked={filters.priorities.includes(option.value)}
210+
onChange={() => togglePriority(option.value)}
211+
className="rounded border-border"
212+
/>
213+
<span className={`font-medium ${option.color}`}>{option.label}</span>
214+
</label>
215+
))}
216+
</div>
217+
)}
218+
</div>
219+
220+
{/* Type Filter */}
221+
<div className="border-t pt-4">
222+
<button
223+
onClick={() => toggleSection('type')}
224+
className="flex items-center justify-between w-full text-sm font-medium mb-2 hover:text-primary transition-colors"
225+
>
226+
<span>Type {filters.types.length > 0 && `(${filters.types.length})`}</span>
227+
<ChevronDown className={`h-4 w-4 transition-transform ${expandedSection === 'type' ? 'rotate-180' : ''}`} />
228+
</button>
229+
{expandedSection === 'type' && (
230+
<div className="space-y-1">
231+
{TYPE_OPTIONS.map((option) => (
232+
<label
233+
key={option.value}
234+
className="flex items-center gap-2 p-2 rounded hover:bg-muted cursor-pointer"
235+
>
236+
<input
237+
type="checkbox"
238+
checked={filters.types.includes(option.value)}
239+
onChange={() => toggleType(option.value)}
240+
className="rounded border-border"
241+
/>
242+
<span>{option.label}</span>
243+
</label>
244+
))}
245+
</div>
246+
)}
247+
</div>
248+
249+
{/* Assignee Filter */}
250+
{availableAssignees.length > 0 && (
251+
<div className="border-t pt-4">
252+
<button
253+
onClick={() => toggleSection('assignee')}
254+
className="flex items-center justify-between w-full text-sm font-medium mb-2 hover:text-primary transition-colors"
255+
>
256+
<span>Assignee {filters.assignees.length > 0 && `(${filters.assignees.length})`}</span>
257+
<ChevronDown className={`h-4 w-4 transition-transform ${expandedSection === 'assignee' ? 'rotate-180' : ''}`} />
258+
</button>
259+
{expandedSection === 'assignee' && (
260+
<div className="space-y-1 max-h-48 overflow-y-auto">
261+
{availableAssignees.map((assignee) => (
262+
<label
263+
key={assignee}
264+
className="flex items-center gap-2 p-2 rounded hover:bg-muted cursor-pointer"
265+
>
266+
<input
267+
type="checkbox"
268+
checked={filters.assignees.includes(assignee)}
269+
onChange={() => toggleAssignee(assignee)}
270+
className="rounded border-border"
271+
/>
272+
<span className="text-sm">{assignee}</span>
273+
</label>
274+
))}
275+
</div>
276+
)}
277+
</div>
278+
)}
279+
280+
{/* Label Filter */}
281+
{availableLabels.length > 0 && (
282+
<div className="border-t pt-4">
283+
<button
284+
onClick={() => toggleSection('labels')}
285+
className="flex items-center justify-between w-full text-sm font-medium mb-2 hover:text-primary transition-colors"
286+
>
287+
<span>Labels {filters.labels.length > 0 && `(${filters.labels.length})`}</span>
288+
<ChevronDown className={`h-4 w-4 transition-transform ${expandedSection === 'labels' ? 'rotate-180' : ''}`} />
289+
</button>
290+
{expandedSection === 'labels' && (
291+
<div className="space-y-1 max-h-48 overflow-y-auto">
292+
{availableLabels.map((label) => (
293+
<label
294+
key={label.id}
295+
className="flex items-center gap-2 p-2 rounded hover:bg-muted cursor-pointer"
296+
>
297+
<input
298+
type="checkbox"
299+
checked={filters.labels.includes(label.id)}
300+
onChange={() => toggleLabel(label.id)}
301+
className="rounded border-border"
302+
/>
303+
<span
304+
className="px-2 py-0.5 text-xs rounded text-white"
305+
style={{ backgroundColor: label.color }}
306+
>
307+
{label.name}
308+
</span>
309+
</label>
310+
))}
311+
</div>
312+
)}
313+
</div>
314+
)}
315+
</Card>
316+
)
317+
}
318+
319+
// Helper function to apply filters to issues
320+
export function applyFilters(issues: Task[], filters: FilterOptions): Task[] {
321+
return issues.filter(issue => {
322+
// Search filter
323+
if (filters.search) {
324+
const searchLower = filters.search.toLowerCase()
325+
const matchesSearch =
326+
issue.title.toLowerCase().includes(searchLower) ||
327+
(issue.description && issue.description.toLowerCase().includes(searchLower)) ||
328+
(issue.key && issue.key.toLowerCase().includes(searchLower))
329+
330+
if (!matchesSearch) return false
331+
}
332+
333+
// Status filter
334+
if (filters.statuses.length > 0 && !filters.statuses.includes(issue.status)) {
335+
return false
336+
}
337+
338+
// Priority filter
339+
if (filters.priorities.length > 0 && !filters.priorities.includes(issue.priority)) {
340+
return false
341+
}
342+
343+
// Type filter
344+
if (filters.types.length > 0) {
345+
const issueType = issue.issue_type || issue.issueType
346+
if (!issueType || !filters.types.includes(issueType)) {
347+
return false
348+
}
349+
}
350+
351+
// Assignee filter
352+
if (filters.assignees.length > 0) {
353+
if (!issue.assignee || !filters.assignees.includes(issue.assignee)) {
354+
return false
355+
}
356+
}
357+
358+
// Label filter
359+
if (filters.labels.length > 0) {
360+
const issueLabels = Array.isArray(issue.labels)
361+
? issue.labels.map(l => typeof l === 'string' ? l : l.id)
362+
: []
363+
364+
const hasMatchingLabel = filters.labels.some(labelId =>
365+
issueLabels.includes(labelId)
366+
)
367+
368+
if (!hasMatchingLabel) return false
369+
}
370+
371+
return true
372+
})
373+
}

0 commit comments

Comments
 (0)