IssueDetail.tsx
Raw
1import type {
2 IssueCommentRow,
3 IssueRow,
4 RepositoryRow,
5} from "../../db/index.ts";
6import { displayName } from "../../lib/users.ts";
7import type { SessionUser } from "../../middleware/session.ts";
8import { Avatar } from "../Avatar.tsx";
9import { DateWithEdited } from "../DateWithEdited.tsx";
10import { Layout } from "../layout.tsx";
11import { ReactionBar, type ReactionCount } from "../ReactionBar.tsx";
12import { RepoHeader } from "../repos/RepoHeader.tsx";
13import { RepoNav } from "../repos/RepoNav.tsx";
14
15interface IssueDetailProps {
16 user: SessionUser | null;
17 repo: RepositoryRow;
18 issue: IssueRow & {
19 author_username: string;
20 author_avatar_version: number | null;
21 };
22 bodyHtml: string;
23 comments: (IssueCommentRow & {
24 author_username: string;
25 author_avatar_version: number | null;
26 bodyHtml: string;
27 })[];
28 reactions: ReactionCount[];
29 commentReactions: Map<number, ReactionCount[]>;
30}
31
32export function IssueDetail({
33 user,
34 repo,
35 issue,
36 bodyHtml,
37 comments,
38 reactions,
39 commentReactions,
40}: IssueDetailProps) {
41 const canEditIssue =
42 user != null &&
43 (user.isAdmin ||
44 (user.id === issue.author_id && issue.status === "open"));
45 const canEditComment = (c: IssueCommentRow) =>
46 user != null &&
47 (user.isAdmin || (user.id === c.author_id && issue.status === "open"));
48 return (
49 <Layout user={user} title={`${issue.title} — ${repo.name}`}>
50 <div class="container">
51 <RepoHeader repo={repo} />
52 <RepoNav repo={repo} active="issues" user={user} />
53 <div class="issue-detail">
54 <div class="issue-detail-header">
55 <span class="issue-number">#{issue.number}</span>
56 <div class="title-with-edit">
57 <h2 class="issue-detail-title">{issue.title}</h2>
58 {canEditIssue && (
59 <>
60 <div class="title-edit-form-area">
61 <form
62 method="POST"
63 action={`/${repo.name}/issues/${issue.number}/edit`}
64 class="title-edit-form"
65 >
66 <input
67 class="form-input"
68 name="title"
69 value={issue.title}
70 required
71 />
72 <input
73 type="hidden"
74 name="edit_body"
75 value={issue.body}
76 />
77 <div class="title-edit-actions">
78 <button
79 type="submit"
80 class="btn btn-sm btn-primary"
81 >
82 Save
83 </button>
84 <button
85 type="button"
86 class="btn btn-sm"
87 onclick="this.closest('.title-with-edit').querySelector('details').removeAttribute('open')"
88 >
89 Cancel
90 </button>
91 </div>
92 </form>
93 </div>
94 <details class="title-edit-details">
95 <summary class="btn btn-xs btn-ghost">
96 Edit title
97 </summary>
98 </details>
99 </>
100 )}
101 </div>
102 <span class={`issue-badge ${issue.status}`}>
103 {issue.status}
104 </span>
105 {canEditIssue && (
106 <div class="issue-detail-meta-actions">
107 {user?.isAdmin && issue.status === "open" && (
108 <form
109 method="POST"
110 action={`/${repo.name}/issues/${issue.number}/complete`}
111 class="inline-form"
112 >
113 <button
114 type="submit"
115 class="btn btn-sm btn-primary"
116 >
117 Completed
118 </button>
119 </form>
120 )}
121 {user?.isAdmin && (
122 <form
123 method="POST"
124 action={`/${repo.name}/issues/${issue.number}/close`}
125 class="inline-form"
126 >
127 <button
128 type="submit"
129 class="btn btn-sm"
130 >
131 {issue.status === "open"
132 ? "Close issue"
133 : "Reopen issue"}
134 </button>
135 </form>
136 )}
137 <details class="confirm-details">
138 <summary class="btn btn-sm btn-danger">
139 Delete
140 </summary>
141 <div class="confirm-popup">
142 Permanently delete this issue?
143 <form
144 method="POST"
145 action={`/${repo.name}/issues/${issue.number}/delete`}
146 class="inline-form"
147 >
148 <button
149 type="submit"
150 class="btn btn-sm btn-danger"
151 >
152 Yes, delete
153 </button>
154 </form>
155 </div>
156 </details>
157 </div>
158 )}
159 </div>
160
161 <div class="timeline-item">
162 <div class="timeline-author">
163 <Avatar
164 userId={issue.author_id}
165 version={issue.author_avatar_version}
166 size={24}
167 />
168 <strong>
169 {displayName(issue.author_username)}
170 </strong>
171 <div class="timeline-author-right">
172 {canEditIssue && (
173 <details class="inline-edit-details">
174 <summary class="btn btn-xs btn-ghost">
175 <span class="when-closed">
176 Edit
177 </span>
178 <span class="when-open">
179 Stop editing
180 </span>
181 </summary>
182 </details>
183 )}
184 <DateWithEdited
185 date={issue.created_at}
186 editedAt={issue.edited_at}
187 />
188 </div>
189 </div>
190 {canEditIssue && (
191 <div class="inline-edit-form-area">
192 <form
193 method="POST"
194 action={`/${repo.name}/issues/${issue.number}/edit`}
195 class="inline-edit-form"
196 >
197 <input
198 type="hidden"
199 name="title"
200 value={issue.title}
201 />
202 <div class="form-group">
203 <textarea
204 class="form-input"
205 id="edit-issue-body"
206 name="edit_body"
207 rows="6"
208 >
209 {issue.body}
210 </textarea>
211 </div>
212 <div class="form-actions">
213 <button
214 type="submit"
215 class="btn btn-sm btn-primary"
216 >
217 Save
218 </button>
219 </div>
220 </form>
221 </div>
222 )}
223 <div class="timeline-body markdown-body">
224 {bodyHtml || (
225 <em class="text-muted">
226 No description provided.
227 </em>
228 )}
229 </div>
230 <ReactionBar
231 reactions={reactions}
232 postUrl={`/${repo.name}/issues/${issue.number}/react`}
233 user={user}
234 />
235 </div>
236
237 {comments.map((comment) => (
238 <div class="timeline-item">
239 <div class="timeline-author">
240 <Avatar
241 userId={comment.author_id}
242 version={comment.author_avatar_version}
243 size={24}
244 />
245 <strong>
246 {displayName(comment.author_username)}
247 </strong>
248 <div class="timeline-author-right">
249 {canEditComment(comment) && (
250 <details class="inline-edit-details">
251 <summary class="btn btn-xs btn-ghost">
252 <span class="when-closed">
253 Edit
254 </span>
255 <span class="when-open">
256 Stop editing
257 </span>
258 </summary>
259 </details>
260 )}
261 <DateWithEdited
262 date={comment.created_at}
263 editedAt={comment.edited_at}
264 />
265 </div>
266 </div>
267 {canEditComment(comment) && (
268 <div class="inline-edit-form-area">
269 <form
270 method="POST"
271 action={`/${repo.name}/issues/${issue.number}/comments/${comment.id}/edit`}
272 class="inline-edit-form"
273 >
274 <div class="form-group">
275 <textarea
276 class="form-input"
277 name="edit_body"
278 rows="6"
279 >
280 {comment.body}
281 </textarea>
282 </div>
283 <div class="form-actions">
284 <button
285 type="submit"
286 class="btn btn-sm btn-primary"
287 >
288 Save
289 </button>
290 </div>
291 </form>
292 </div>
293 )}
294 <div class="timeline-body markdown-body">
295 {comment.bodyHtml}
296 </div>
297 <ReactionBar
298 reactions={
299 commentReactions.get(comment.id) ?? []
300 }
301 postUrl={`/${repo.name}/issues/${issue.number}/react`}
302 commentId={comment.id}
303 user={user}
304 />
305 </div>
306 ))}
307
308 {user && (
309 <div class="timeline-item timeline-item-new">
310 <h3 class="section-title">Add a comment</h3>
311 <form
312 method="POST"
313 action={`/${repo.name}/issues/${issue.number}/comments`}
314 >
315 <div class="form-group">
316 <textarea
317 name="body"
318 rows="6"
319 placeholder="Leave a comment (Markdown supported)"
320 required
321 />
322 </div>
323 <div class="form-actions">
324 <button
325 type="submit"
326 class="btn btn-primary"
327 >
328 Comment
329 </button>
330 </div>
331 </form>
332 </div>
333 )}
334 {!user && (
335 <p class="text-muted">
336 <a href="/login">Sign in</a> to leave a comment.
337 </p>
338 )}
339 </div>
340 </div>
341 </Layout>
342 );
343}
344