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