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 safe
119 >
120 {label.name}
121 </span>
122 </label>
123 ))}
124 </div>
125 <div class="label-filter-actions">
126 <button
127 type="submit"
128 class="btn btn-primary btn-sm"
129 >
130 Apply
131 </button>
132 <a
133 href={`/${repo.name}/issues?status=${status}`}
134 class="btn btn-secondary btn-sm"
135 >
136 Clear
137 </a>
138 </div>
139 </form>
140 </div>
141 </details>
142 )}
143 {!!user && (
144 <a
145 href={`/${repo.name}/issues/new`}
146 class="btn btn-primary btn-sm"
147 >
148 New issue
149 </a>
150 )}
151 </div>
152 </div>
153 {issues.length === 0 ? (
154 <div class="empty-state">
155 <p>No {status} issues.</p>
156 </div>
157 ) : (
158 <ul class="issue-list">
159 {issues.map((issue) => {
160 const labels = labelsByIssueId.get(issue.id) ?? [];
161 const unsafeAuthor = displayName(
162 issue.author_username,
163 );
164 return (
165 <li class="issue-item">
166 <div class="issue-main">
167 <span
168 class={`issue-status-dot ${issue.status}`}
169 />
170 <a
171 href={`/${repo.name}/issues/${issue.number}`}
172 class="issue-title"
173 safe
174 >
175 {issue.title}
176 </a>
177 <span class="issue-number">
178 #{issue.number}
179 </span>
180 </div>
181 {labels.length > 0 && (
182 <div class="issue-labels">
183 {labels.map((label) => (
184 <span
185 class="label-badge"
186 style={`background:${label.color};color:${labelTextColor(label.color)}`}
187 safe
188 >
189 {label.name}
190 </span>
191 ))}
192 </div>
193 )}
194 <div class="issue-meta">
195 <Avatar
196 userId={issue.author_id}
197 version={
198 issue.author_avatar_version
199 }
200 size={20}
201 />
202 <span class="issue-author" safe>
203 opened by {unsafeAuthor}
204 </span>
205 <time datetime={issue.created_at} safe>
206 {formatDate(issue.created_at)}
207 </time>
208 </div>
209 </li>
210 );
211 })}
212 </ul>
213 )}
214 <Pagination {...pagination} />
215 </div>
216 </Layout>
217 );
218}
219