Add epic filter to kanban board
- Add epicId filter to KanbanFilterOptions - Filter issues by parent epic when epicId is set - Add epic dropdown to kanban controls with title-first format - Pass epics list from kanban page to controls
This commit is contained in:
parent
2cfaa9b406
commit
74871545c7
3 changed files with 28 additions and 0 deletions
|
|
@ -3,12 +3,14 @@
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
import type { KanbanFilterOptions, KanbanStats } from '../../lib/kanban';
|
import type { KanbanFilterOptions, KanbanStats } from '../../lib/kanban';
|
||||||
|
import type { BeadIssue } from '../../lib/types';
|
||||||
|
|
||||||
import { StatPill } from '../shared/stat-pill';
|
import { StatPill } from '../shared/stat-pill';
|
||||||
|
|
||||||
interface KanbanControlsProps {
|
interface KanbanControlsProps {
|
||||||
filters: KanbanFilterOptions;
|
filters: KanbanFilterOptions;
|
||||||
stats: KanbanStats;
|
stats: KanbanStats;
|
||||||
|
epics: BeadIssue[];
|
||||||
onFiltersChange: (filters: KanbanFilterOptions) => void;
|
onFiltersChange: (filters: KanbanFilterOptions) => void;
|
||||||
onNextActionable: () => void;
|
onNextActionable: () => void;
|
||||||
nextActionableFeedback?: string | null;
|
nextActionableFeedback?: string | null;
|
||||||
|
|
@ -17,6 +19,7 @@ interface KanbanControlsProps {
|
||||||
export function KanbanControls({
|
export function KanbanControls({
|
||||||
filters,
|
filters,
|
||||||
stats,
|
stats,
|
||||||
|
epics,
|
||||||
onFiltersChange,
|
onFiltersChange,
|
||||||
onNextActionable,
|
onNextActionable,
|
||||||
nextActionableFeedback = null,
|
nextActionableFeedback = null,
|
||||||
|
|
@ -34,6 +37,19 @@ export function KanbanControls({
|
||||||
placeholder="Search by id/title/labels"
|
placeholder="Search by id/title/labels"
|
||||||
className={`${inputClass} w-full sm:min-w-[18rem] sm:flex-1`}
|
className={`${inputClass} w-full sm:min-w-[18rem] sm:flex-1`}
|
||||||
/>
|
/>
|
||||||
|
<select
|
||||||
|
value={filters.epicId ?? ''}
|
||||||
|
onChange={(event) => onFiltersChange({ ...filters, epicId: event.target.value || undefined })}
|
||||||
|
className={`${inputClass} ui-select w-full sm:w-52`}
|
||||||
|
aria-label="Epic filter"
|
||||||
|
>
|
||||||
|
<option className="ui-option" value="">All epics</option>
|
||||||
|
{(epics ?? []).map((epic) => (
|
||||||
|
<option className="ui-option" key={epic.id} value={epic.id}>
|
||||||
|
{epic.title.slice(0, 40)}{epic.title.length > 40 ? '…' : ''} — {epic.id}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
<select
|
<select
|
||||||
value={filters.type ?? ''}
|
value={filters.type ?? ''}
|
||||||
onChange={(event) => onFiltersChange({ ...filters, type: event.target.value })}
|
onChange={(event) => onFiltersChange({ ...filters, type: event.target.value })}
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,7 @@ export function KanbanPage({
|
||||||
<KanbanControls
|
<KanbanControls
|
||||||
filters={filters}
|
filters={filters}
|
||||||
stats={stats}
|
stats={stats}
|
||||||
|
epics={localIssues.filter((issue) => issue.issue_type === 'epic')}
|
||||||
onFiltersChange={setFilters}
|
onFiltersChange={setFilters}
|
||||||
onNextActionable={handleNextActionable}
|
onNextActionable={handleNextActionable}
|
||||||
nextActionableFeedback={nextActionableFeedback}
|
nextActionableFeedback={nextActionableFeedback}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ export interface KanbanFilterOptions {
|
||||||
type?: string;
|
type?: string;
|
||||||
priority?: string;
|
priority?: string;
|
||||||
showClosed?: boolean;
|
showClosed?: boolean;
|
||||||
|
epicId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KanbanStats {
|
export interface KanbanStats {
|
||||||
|
|
@ -123,6 +124,7 @@ export function filterKanbanIssues(issues: BeadIssue[], filters: KanbanFilterOpt
|
||||||
const type = (filters.type ?? '').trim().toLowerCase();
|
const type = (filters.type ?? '').trim().toLowerCase();
|
||||||
const priority = (filters.priority ?? '').trim();
|
const priority = (filters.priority ?? '').trim();
|
||||||
const showClosed = filters.showClosed ?? false;
|
const showClosed = filters.showClosed ?? false;
|
||||||
|
const epicId = filters.epicId?.trim();
|
||||||
|
|
||||||
return issues.filter((issue) => {
|
return issues.filter((issue) => {
|
||||||
if (!showClosed && issue.status === 'closed') {
|
if (!showClosed && issue.status === 'closed') {
|
||||||
|
|
@ -144,6 +146,15 @@ export function filterKanbanIssues(issues: BeadIssue[], filters: KanbanFilterOpt
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (epicId) {
|
||||||
|
// Filter to show only tasks belonging to this epic
|
||||||
|
const parentDep = issue.dependencies.find((dep) => dep.type === 'parent');
|
||||||
|
const issueEpicId = parentDep?.target ?? (issue.id.includes('.') ? issue.id.split('.')[0] : null);
|
||||||
|
if (issueEpicId !== epicId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue