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