IssueList.tsx
Raw
1import type { IssueRow, LabelRow, RepositoryRow } from "../../db/index.ts";
2import { formatDate } from "../../lib/formatDate.ts";
3import { labelTextColor } from "../../lib/labelColor.ts";
4import { displayName } from "../../lib/users.ts";
5import type { SessionUser } from "../../middleware/session.ts";
6import { Avatar } from "../Avatar.tsx";
7import { Layout } from "../layout.tsx";
8import { Pagination, type PaginationInfo } from "../Pagination.tsx";
9import { RepoHeader } from "../repos/RepoHeader.tsx";
10import { RepoNav } from "../repos/RepoNav.tsx";
11
12interface IssueListProps {
13 user: SessionUser | null;
14 repo: RepositoryRow;
15 issues: (IssueRow & {
16 author_username: string;
17 author_avatar_version: number | null;
18 })[];
19 status: "open" | "closed" | "completed";
20 counts: Record<string, number>;
21 pagination: PaginationInfo;
22 repoLabels: LabelRow[];
23 selectedLabelIds: number[];
24 labelsByIssueId: Map<number, LabelRow[]>;
25}
26
27export function IssueList({
28 user,
29 repo,
30 issues,
31 status,
32 counts,
33 pagination,
34 repoLabels,
35 selectedLabelIds,
36 labelsByIssueId,
37}: IssueListProps) {
38 const labelsParam =
39 selectedLabelIds.length > 0
40 ? `&labels=${selectedLabelIds.map(String).join(",")}`
41 : "";
42 return (
43 <Layout user={user} title={`Issues — ${repo.name}`}>
44 <div class="container">
45 <RepoHeader repo={repo} />
46 <RepoNav repo={repo} active="issues" user={user} />
47 <div class="list-header">
48 <div class="list-header-tabs">
49 <a
50 href={`/${repo.name}/issues?status=open${labelsParam}`}
51 class={`list-tab${status === "open" ? " active" : ""}`}
52 >
53 Open{" "}
54 {(counts.open ?? 0) > 0 && (
55 <span class="tab-count">{counts.open}</span>
56 )}
57 </a>
58 <a
59 href={`/${repo.name}/issues?status=completed${labelsParam}`}
60 class={`list-tab${status === "completed" ? " active" : ""}`}
61 >
62 Completed{" "}
63 {(counts.completed ?? 0) > 0 && (
64 <span class="tab-count">
65 {counts.completed}
66 </span>
67 )}
68 </a>
69 <a
70 href={`/${repo.name}/issues?status=closed${labelsParam}`}
71 class={`list-tab${status === "closed" ? " active" : ""}`}
72 >
73 Closed{" "}
74 {(counts.closed ?? 0) > 0 && (
75 <span class="tab-count">{counts.closed}</span>
76 )}
77 </a>
78 </div>
79 <div class="list-header-actions">
80 {repoLabels.length > 0 && (
81 <details class="label-filter">
82 <summary class="btn btn-secondary btn-sm">
83 Filter by label
84 {selectedLabelIds.length > 0 && (
85 <span class="tab-count">
86 {selectedLabelIds.length}
87 </span>
88 )}
89 </summary>
90 <div class="label-filter-popup">
91 <form
92 method="GET"
93 action={`/${repo.name}/issues`}
94 >
95 <input
96 type="hidden"
97 name="status"
98 value={status}
99 />
100 <div class="label-filter-list">
101 {repoLabels.map((label) => (
102 <label class="label-filter-item">
103 <input
104 type="checkbox"
105 name="labels"
106 value={String(label.id)}
107 checked={
108 selectedLabelIds.includes(
109 label.id,
110 )
111 ? true
112 : undefined
113 }
114 />
115 <span
116 class="label-badge"
117 style={`background:${label.color};color:${labelTextColor(label.color)}`}
118 >
119 {label.name}
120 </span>
121 </label>
122 ))}
123 </div>
124 <div class="label-filter-actions">
125 <button
126 type="submit"
127 class="btn btn-primary btn-sm"
128 >
129 Apply
130 </button>
131 <a
132 href={`/${repo.name}/issues?status=${status}`}
133 class="btn btn-secondary btn-sm"
134 >
135 Clear
136 </a>
137 </div>
138 </form>
139 </div>
140 </details>
141 )}
142 {user && (
143 <a
144 href={`/${repo.name}/issues/new`}
145 class="btn btn-primary btn-sm"
146 >
147 New issue
148 </a>
149 )}
150 </div>
151 </div>
152 {issues.length === 0 ? (
153 <div class="empty-state">
154 <p>No {status} issues.</p>
155 </div>
156 ) : (
157 <ul class="issue-list">
158 {issues.map((issue) => {
159 const labels = labelsByIssueId.get(issue.id) ?? [];
160 return (
161 <li class="issue-item">
162 <div class="issue-main">
163 <span
164 class={`issue-status-dot ${issue.status}`}
165 />
166 <a
167 href={`/${repo.name}/issues/${issue.number}`}
168 class="issue-title"
169 >
170 {issue.title}
171 </a>
172 <span class="issue-number">
173 #{issue.number}
174 </span>
175 </div>
176 {labels.length > 0 && (
177 <div class="issue-labels">
178 {labels.map((label) => (
179 <span
180 class="label-badge"
181 style={`background:${label.color};color:${labelTextColor(label.color)}`}
182 >
183 {label.name}
184 </span>
185 ))}
186 </div>
187 )}
188 <div class="issue-meta">
189 <Avatar
190 userId={issue.author_id}
191 version={issue.author_avatar_version}
192 size={20}
193 />
194 <span class="issue-author">
195 opened by{" "}
196 {displayName(issue.author_username)}
197 </span>
198 <time datetime={issue.created_at}>
199 {formatDate(issue.created_at)}
200 </time>
201 </div>
202 </li>
203 );
204 })}
205 </ul>
206 )}
207 <Pagination {...pagination} />
208 </div>
209 </Layout>
210 );
211}
212