Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 0 additions & 76 deletions frontend/src/components/IssueBacklog.test.tsx
Original file line number Diff line number Diff line change
@@ -1,79 +1,3 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { IssueBacklog } from "./IssueBacklog";
import { OpenIssue } from "../types/stream";

const issues: OpenIssue[] = [
{
id: "medium-api",
title: "Add API pagination",
labels: ["api", "backend"],
summary: "Paginate API responses.",
complexity: "Medium",
points: 150,
},
{
id: "trivial-docs",
title: "Document local setup",
labels: ["documentation"],
summary: "Add local setup notes.",
complexity: "Trivial",
points: 100,
},
{
id: "high-ui",
title: "Improve dashboard filters",
labels: ["frontend", "ux"],
summary: "Add better filtering controls.",
complexity: "High",
points: 200,
},
];

describe("IssueBacklog", () => {
it("sorts issues by points from high to low by default", () => {
render(<IssueBacklog issues={issues} />);

const titles = screen
.getAllByRole("heading", { level: 3 })
.map((heading) => heading.textContent);

expect(titles).toEqual([
"Improve dashboard filters",
"Add API pagination",
"Document local setup",
]);
});

it("filters issues by label", () => {
render(<IssueBacklog issues={issues} />);

fireEvent.change(screen.getByLabelText("Filter issues by label"), {
target: { value: "documentation" },
});

expect(screen.getByText("Document local setup")).toBeInTheDocument();
expect(screen.queryByText("Add API pagination")).not.toBeInTheDocument();
expect(
screen.queryByText("Improve dashboard filters"),
).not.toBeInTheDocument();
});

it("sorts issues by title", () => {
render(<IssueBacklog issues={issues} />);

fireEvent.change(screen.getByLabelText("Sort issues"), {
target: { value: "title" },
});

const titles = screen
.getAllByRole("heading", { level: 3 })
.map((heading) => heading.textContent);

expect(titles).toEqual([
"Add API pagination",
"Document local setup",
"Improve dashboard filters",
]);
});
});
77 changes: 10 additions & 67 deletions frontend/src/components/IssueBacklog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { useMemo, useState } from "react";

import { OpenIssue } from "../types/stream";

type SortKey = "none" | "complexity-asc" | "points-asc" | "points-desc";

const COMPLEXITY_ORDER: Record<OpenIssue["complexity"], number> = {
Trivial: 0,
Medium: 1,
High: 2,
};

interface IssueBacklogProps {
issues: OpenIssue[];
loading?: boolean;
Expand All @@ -15,47 +23,15 @@ const complexityRank: Record<OpenIssue["complexity"], number> = {
};

export function IssueBacklog({ issues, loading }: IssueBacklogProps) {
const [selectedLabel, setSelectedLabel] = useState("");
const [sortBy, setSortBy] = useState<SortOption>("points-desc");

const labels = useMemo(
() => Array.from(new Set(issues.flatMap((issue) => issue.labels))).sort(),
[issues],
);

const visibleIssues = useMemo(() => {
const filtered = selectedLabel
? issues.filter((issue) => issue.labels.includes(selectedLabel))
: issues;

return [...filtered].sort((a, b) => {
if (sortBy === "points-asc") {
return a.points - b.points;
}

if (sortBy === "complexity") {
return complexityRank[a.complexity] - complexityRank[b.complexity];
}

if (sortBy === "title") {
return a.title.localeCompare(b.title);
}

return b.points - a.points;
});
}, [issues, selectedLabel, sortBy]);

if (loading) {
return (
<div className="card">
<h2>Maintainer Backlog</h2>
<div className="activity-feed">
{[1, 2, 3].map((i) => (
<div
key={i}
className="skeleton skeleton-item"
style={{ height: "100px" }}
/>

))}
</div>
</div>
Expand All @@ -65,40 +41,7 @@ export function IssueBacklog({ issues, loading }: IssueBacklogProps) {
return (
<div className="card">
<h2>Maintainer Backlog</h2>
<p className="muted">
Open these as GitHub issues after publishing the repository.
</p>

<div className="filter-bar" style={{ marginBottom: "1rem" }}>
<label>
Label
<select
aria-label="Filter issues by label"
value={selectedLabel}
onChange={(event) => setSelectedLabel(event.target.value)}
>
<option value="">All labels</option>
{labels.map((label) => (
<option key={label} value={label}>
{label}
</option>
))}
</select>
</label>

<label>
Sort by
<select
aria-label="Sort issues"
value={sortBy}
onChange={(event) => setSortBy(event.target.value as SortOption)}
>
<option value="points-desc">Points: high to low</option>
<option value="points-asc">Points: low to high</option>
<option value="complexity">Complexity</option>
<option value="title">Title</option>
</select>
</label>
</div>

{visibleIssues.length === 0 ? (
Expand Down
Loading